Un client de base de données a un problème fondamental : il doit afficher des résultats qui peuvent aller de 10 lignes à 10 millions. Charger l'intégralité d'un résultat en mémoire avant de l'afficher est la solution la plus simple, et c'est exactement ce que font beaucoup d'outils. Ça fonctionne jusqu'au jour où un développeur lance un SELECT * sur une table de production sans clause WHERE.
Dans QoreDB, les résultats ne sont jamais accumulés intégralement côté backend. Chaque ligne est transmise au frontend dès qu'elle est disponible, via un pipeline de streaming qui maintient une consommation mémoire constante quelle que soit la taille du jeu de données.
Un canal borné entre le driver et le frontend
Le mécanisme central repose sur un canal tokio borné à 100 éléments. Côté driver, chaque ligne lue depuis le curseur de la base est envoyée dans ce canal sous forme d'événement typé (StreamEvent). Côté consommateur, les événements sont lus et transmis au frontend via l'IPC Tauri.
Le canal borné introduit naturellement du backpressure : quand le consommateur ne lit pas assez vite, le canal se remplit et le driver se met en pause. Il n'y a pas de buffering illimité, pas d'accumulation silencieuse. Si le frontend ralentit, le driver ralentit aussi. Ce mécanisme est le même pour tous les moteurs supportés.
Le protocole de streaming est structuré en quatre types d'événements : Columns (envoyé une seule fois au début avec les métadonnées des colonnes), Row (une ligne de données), Error (en cas de problème pendant le parcours), et Done (fin du flux avec le nombre total de lignes). Cette séparation permet au frontend de commencer le rendu dès la première ligne reçue, sans attendre la fin de la requête.
Des curseurs natifs par driver
Chaque driver implémente le streaming en utilisant les curseurs natifs de son moteur. PostgreSQL, MySQL/MariaDB, SQLite, DuckDB et SQL Server exposent tous une méthode execute_stream sur le trait DataEngine. L'implémentation PostgreSQL, par exemple, utilise la méthode fetch() de SQLx qui retourne un Stream Rust asynchrone. Chaque ligne est convertie et envoyée dans le canal au fur et à mesure.
Ce choix de déléguer aux curseurs natifs est important. QoreDB ne charge pas N lignes dans un Vec pour les retransmettre ensuite. Le parcours est incrémental : une ligne entre dans le canal, une ligne en sort. La mémoire utilisée par le pipeline de streaming est proportionnelle à la taille du canal (100 éléments), pas à la taille du résultat.
Le trait DataEngine déclare aussi une méthode supports_streaming() qui permet au frontend de savoir si le driver actif supporte ce mode. Quand le streaming n'est pas disponible, l'exécution retombe sur le chemin classique avec un résultat complet en mémoire, mais borné par la pagination.
L'export comme cas d'usage critique
Le streaming prend tout son sens lors de l'export de données. Exporter une table de plusieurs millions de lignes en CSV, JSON ou Parquet est une opération courante. Sans streaming, il faudrait tout charger en mémoire avant d'écrire le fichier. Avec le pipeline d'export de QoreDB, les lignes sont écrites dans le fichier de sortie au fil de leur arrivée.
Le pipeline d'export utilise le même canal borné à 100 éléments. Un batch_size configurable (1000 lignes par défaut) contrôle la fréquence de flush du writer. L'utilisateur peut aussi définir une limite de lignes, auquel cas le driver est annulé proprement une fois la limite atteinte. Le tout s'exécute dans une tâche Tokio dédiée, sans bloquer l'interface.
La progression est émise vers le frontend toutes les 250 millisecondes au maximum, pour éviter de saturer le canal IPC Tauri avec des mises à jour trop fréquentes. Ce throttling est volontaire : sur un export de 5 millions de lignes, émettre un événement par ligne surchargerait la webview.
Côté frontend : chargement incrémental et virtualisation
Le frontend complète le dispositif avec un hook useInfiniteTableData qui charge les données par morceaux de 100 lignes. Quand l'utilisateur scrolle vers le bas de la grille, le chunk suivant est demandé au backend via une requête paginée (OFFSET/LIMIT déléguée au moteur). Les lignes s'accumulent dans le state React et la grille les affiche via la virtualisation DOM, ce qui signifie que seules les lignes visibles à l'écran sont réellement rendues.
La pagination côté backend est elle aussi bornée. Le page_size est limité à 10 000 lignes maximum par requête, via un clamp explicite dans le code. Ce n'est pas un choix arbitraire : au-delà de ce seuil, la sérialisation JSON pour le transit IPC et le rendu frontend deviennent des goulots d'étranglement sur une application desktop.
En pratique : ce que voit l'utilisateur
Pour l'utilisateur, tout cela est transparent. Quand il exécute une requête, les premières lignes apparaissent quasi instantanément. En scrollant, de nouvelles lignes se chargent à la demande. Quand il lance un export, une barre de progression s'affiche avec le nombre de lignes traitées, et il peut annuler l'opération à tout moment.
Le streaming permet aussi l'annulation propre. Si l'utilisateur clique sur "Stop" pendant une requête longue, le token d'annulation est propagé jusqu'au driver, qui interrompt le parcours du curseur. Sur PostgreSQL, cela se traduit par un appel à pg_cancel_backend. Sur MySQL, par un KILL QUERY. Le canal borné garantit que la mémoire est libérée rapidement une fois l'annulation effective.
Le streaming de résultats dans QoreDB n'est pas une optimisation cosmétique. C'est une contrainte d'architecture qui traverse toutes les couches, du curseur natif du moteur jusqu'à la virtualisation DOM du frontend. Le résultat est un client qui peut manipuler des jeux de données volumineux sans que la consommation mémoire ne devienne proportionnelle à leur taille.
Restez informé des nouveautés
Rejoignez notre newsletter pour recevoir les mises à jour majeures, les nouveaux drivers et nos coulisses techniques.




