clickhouse-rust-type-mismatches

Par divinevideo · divine-mobile

Corrigez les erreurs de requête ClickHouse en Rust lors de l'utilisation du crate `clickhouse-rs`. À utiliser dans les cas suivants : (1) Erreurs "string is not valid utf8" - les colonnes `FixedString` nécessitent généralement un `CAST` vers `String`, (2) Erreurs "tag for enum is not valid" - des champs `Option<T>` reçoivent généralement des valeurs non-NULL, (3) Les agrégations Sum/count renvoient `Float64` alors que Rust attend `u64`, (4) Les résultats de `LEFT JOIN` contiennent des chaînes vides là où Rust attend `Option::None`, (5) "not enough data, probably a row type mismatches a database schema" - le `SELECT` de la requête renvoie moins de colonnes que le struct `Row` Rust ne l'attend (fréquent quand plusieurs fonctions de requête partagent le même struct, mais qu'une fonction ne possède pas les colonnes ajoutées ultérieurement), (6) Données tronquées/corrompues lors d'un `INSERT` vers des colonnes `FixedString(N)` en utilisant le type Rust `String` - corrompt silencieusement les données (sans erreur !) car `String` ajoute un préfixe de longueur alors que `FixedString` attend exactement N octets bruts. Correctif : utiliser `[u8; N]` avec `#[serde(with = "BigArray")]` du crate `serde-big-array`, (7) "the trait bound `[u8; 64]: serde::Serialize` is not satisfied" lors de l'utilisation de tableaux `[u8; N]` dans les structs `Row` - l'annotation `#[serde(with = "BigArray")]` du crate `serde-big-array` est requise.

npx skills add https://github.com/divinevideo/divine-mobile --skill clickhouse-rust-type-mismatches

Décalages de Type ClickHouse-Rust

Problème

Lors de l'utilisation de la crate Rust clickhouse-rs avec ClickHouse, des erreurs de désérialisation surviennent en raison de décalages de type entre le schéma de la base de données et les définitions de struct Rust.

Contexte / Conditions de Déclenchement

  • Erreur : « string is not valid utf8 » lors de l'interrogation de colonnes String
  • Erreur : « tag for enum is not valid » lors de la désérialisation de lignes
  • Erreur : « not enough data, probably a row type mismatches a database schema » lors de la désérialisation de lignes
  • Utilisation de #[derive(Row)] depuis la crate clickhouse
  • Vues utilisant LEFT JOIN ou des fonctions d'agrégation
  • Plusieurs fonctions de requête partageant le même struct Row mais avec différentes listes SELECT

Causes Racines

0. Décalage du Nombre de Colonnes (« not enough data »)

Quand plusieurs fonctions de requête partagent le même struct #[derive(Row)], ajouter de nouveaux champs au struct et à certaines requêtes tout en oubliant de mettre à jour D'AUTRES requêtes utilisant le même struct provoque des erreurs de désérialisation « not enough data ». La requête retourne moins de colonnes que le struct n'en attend.

Symptômes :

  • Erreur : « not enough data, probably a row type mismatches a database schema »
  • Une fonction de requête fonctionne, une autre échoue, les deux utilisant le même struct Row
  • L'erreur est trompeuse — suggère un problème de schéma DB mais c'est en réalité un bug de code

Débogage :

  1. Compter les champs du struct Rust Row
  2. Compter les colonnes SELECT de la requête défaillante
  3. Comparer avec la requête fonctionnelle — chercher les colonnes manquantes
  4. Souvent causé par l'ajout de champs (comme des colonnes subtitle/text-track) au struct et à une requête mais l'oubli de mise à jour d'une requête secondaire (ex. get_by_id mis à jour mais get_by_d_tag oublié)

Correction : Ajouter les colonnes SELECT manquantes à la requête défaillante pour correspondre au struct :

// Les deux requêtes doivent sélectionner les MÊMES colonnes dans le MÊME ordre que le struct
// Si le struct a 17 champs, chaque requête l'utilisant doit SELECT 17 colonnes

1a. FixedString vs String (SELECT / Désérialisation)

ClickHouse FixedString(N) complète avec des bytes null. Ceux-ci ne se désérialisent pas proprement en tant que chaînes UTF-8 en Rust.

Correction : Convertir en String dans la vue SQL :

SELECT CAST(pubkey AS String) AS pubkey FROM ...

1b. FixedString vs String (INSERT / Sérialisation) — CORRUPTION SILENCIEUSE DE DONNÉES

C'est la variante la plus dangereuse car il N'Y A PAS de message d'erreur. Lors de l'utilisation de String dans un struct Rust #[derive(Row, Serialize)] pour une colonne ClickHouse FixedString(N), le protocole binaire clickhouse-rs sérialise String avec un préfixe de longueur varint, mais FixedString(N) attend exactement N bytes bruts sans préfixe. Le(s) byte(s) supplémentaire(s) décalent toutes les données de colonne suivantes, produisant des lignes brouillées où les données saignent entre les colonnes.

Symptômes :

  • Aucune erreur lors de l'INSERT — les données semblent s'écrire correctement
  • L'interrogation du tableau montre des données brouillées avec colonnes décalées/mélangées
  • Les vues matérialisées construites sur le tableau contiennent des agrégations brouillées
  • Souvent découvert seulement quand les requêtes aval retournent des résultats insensés

Correction : Utiliser [u8; N] avec #[serde(with = "BigArray")] de la crate serde-big-array :

use serde_big_array::BigArray;

#[derive(Row, Serialize)]
pub struct MyInsertRow {
    #[serde(with = "BigArray")]
    pub event_id: [u8; 64],   // FixedString(64)
    #[serde(with = "BigArray")]
    pub pubkey: [u8; 64],     // FixedString(64)
    pub name: String,          // Colonne String — très bien tel quel
}

/// Helper pour convertir des chaînes hex en tableaux de bytes fixes
pub fn hex_string_to_fixed64(hex: &str) -> [u8; 64] {
    let mut buf = [0u8; 64];
    let bytes = hex.as_bytes();
    let len = bytes.len().min(64);
    buf[..len].copy_from_slice(&bytes[..len]);
    buf
}

IMPORTANT : Utiliser [u8; 64] sans #[serde(with = "BigArray")] échouera avec :

error[E0277]: the trait bound `[u8; 64]: serde::Serialize` is not satisfied

C'est parce que la dépendance serde de la crate clickhouse ne supporte pas la sérialisation de tableaux avec génériques de const pour les tableaux > 32 éléments. L'annotation BigArray est requise.

2. Option<T> vs Colonnes Non-Nullables

Quand ClickHouse retourne des chaînes vides "" de LEFT JOINs (parce que le tableau joint a des colonnes String non-nullables), Rust Option<String> attend de véritables valeurs NULL, pas des chaînes vides.

Correction : Utiliser String au lieu de Option<String> dans les structs Rust :

// Incorrect - cause "tag for enum is not valid"
pub name: Option<String>,

// Correct - gère les chaînes vides de LEFT JOIN
pub name: String,

3. Résultats d'Agrégation Float64

Les agrégations de somme/comptage sur certains types de colonnes retournent Float64, pas UInt64.

Correction : Utiliser f64 dans le struct Rust :

// Incorrect
pub loops: u64,

// Correct
pub loops: f64,

Solution

1. Vérifier les types de colonnes ClickHouse

DESCRIBE TABLE your_view FORMAT TabSeparated

2. Faire correspondre les types Rust aux types ClickHouse

Pour SELECT (désérialisation) : | Type ClickHouse | Type Rust | |-----------------|-----------| | String | String | | FixedString(N) | String (avec CAST en SQL) ou [u8; N] avec BigArray | | UInt64 | u64 | | Float64 | f64 | | Nullable(String) | Option<String> | | String de LEFT JOIN | String (pas Option<String>) |

Pour INSERT (sérialisation) : | Type ClickHouse | Type Rust | Notes | |-----------------|-----------|-------| | String | String | Fonctionne tel quel | | FixedString(N) | [u8; N] + #[serde(with = "BigArray")] | NE JAMAIS utiliser String — cause la corruption silencieuse | | UInt64 | u64 | Fonctionne tel quel | | DateTime | DateTime<Utc> + #[serde(with = "clickhouse::serde::chrono::datetime")] | |

3. Corriger les vues pour convertir les types

CREATE VIEW fixed_view AS
SELECT
    CAST(pubkey AS String) AS pubkey,  -- Corriger FixedString
    name,  -- String reste String, gérer le vide en Rust
    loops  -- Float64 correspond à f64 en Rust
FROM ...

Vérification

Interroger directement via l'interface HTTP pour confirmer le format de données :

curl -u 'user:pass' 'https://clickhouse-host:8443' \
  --data-binary "SELECT * FROM your_view LIMIT 1 FORMAT JSONEachRow"

Exemple

Schéma ClickHouse :

pubkey FixedString(64)
name String  -- de LEFT JOIN, peut être vide ""
loops Float64

Struct Rust incorrect :

pub struct Entry {
    pub pubkey: String,       // Échoue : remplissage null de FixedString
    pub name: Option<String>, // Échoue : "" n'est pas NULL
    pub loops: u64,           // Échoue : Float64 pas UInt64
}

Struct Rust correct + SQL :

// Struct
pub struct Entry {
    pub pubkey: String,  // Fonctionne avec CAST
    pub name: String,    // Gère les chaînes vides
    pub loops: f64,      // Correspond à Float64
}
-- Vue
SELECT CAST(pubkey AS String) AS pubkey, name, loops FROM ...

Notes

  • La crate clickhouse-rs utilise le protocole binaire, pas JSON, donc les erreurs apparaissent à la désérialisation
  • Les colonnes *State d'AggregatingMergeTree nécessitent les fonctions *Merge() dans les requêtes
  • Exécuter DESCRIBE TABLE pour voir les types de colonnes exacts
  • Tester les requêtes via l'interface HTTP d'abord pour isoler les problèmes de schéma vs client

Références

Skills similaires