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 crateclickhouse - 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 :
- Compter les champs du struct Rust
Row - Compter les colonnes SELECT de la requête défaillante
- Comparer avec la requête fonctionnelle — chercher les colonnes manquantes
- 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_idmis à jour maisget_by_d_tagoublié)
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
*Stated'AggregatingMergeTree nécessitent les fonctions*Merge()dans les requêtes - Exécuter
DESCRIBE TABLEpour 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