Intégrer DuckDB dans Windev : Guide pratique avec wrapper .NET C#

10 min de lecture
Rédigé par Laurent Glesner - Consultant PC SOFT chez EloNeva
windev duckdb dotnet

En bref

  • Pourquoi ODBC et DLL natives ne suffisent pas pour DuckDB dans Windev.
  • Développement d'un wrapper .NET C# pour encapsuler les opérations DuckDB.
  • Export CSV depuis Windev, import bulk dans DuckDB, et requêtes JSON optimisées.
  • Projet complet disponible sur GitHub avec code source et exemples.

Introduction

Dans un précédent article, nous avons présenté les bénéfices de DuckDB comme moteur OLAP pour analyser de gros volumes de données, en complément d’une base OLTP comme HFSQL. Mais comment, concrètement, intégrer DuckDB dans une application Windev ?

Windev permet d’appeler des bibliothèques externes via ODBC, API C/C++, ou assemblages .NET. Dans cet article, nous allons voir pourquoi l’approche par wrapper .NET C# est la plus efficace, et comment l’implémenter pas à pas.

Nous couvrirons :

  • Les limitations des approches ODBC et DLL natives,
  • L’architecture complète de la solution,
  • Le code du wrapper .NET avec les trois méthodes clés,
  • L’utilisation depuis Windev avec des exemples concrets en WLangage,
  • Les performances mesurées sur des volumétries réelles,
  • Les bonnes pratiques et points d’attention.

Le code complet est disponible sur GitHub pour vous permettre de démarrer rapidement.


1. Pourquoi un wrapper .NET pour DuckDB dans Windev ?

1.1 Les limitations de l’approche ODBC

DuckDB fournit bien un driver ODBC, mais son utilisation depuis Windev pose plusieurs problèmes :

  • Configuration complexe : Installation du driver sur chaque poste, gestion des DSN (Data Source Name) système ou utilisateur.
  • Chaînes de connexion fastidieuses : DSN-less possible mais syntaxe lourde à maintenir.
  • Performances bulk insert médiocres : L’import de millions de lignes via ODBC est lent comparé à la commande native COPY.
  • Gestion d’erreurs limitée : Les codes d’erreur ODBC sont génériques, difficiles à interpréter.
  • Pas d’accès aux fonctionnalités avancées : Paramètres DuckDB (PRAGMA), extensions, requêtes avec COPY directement, etc.

1.2 Les limitations de l’approche DLL native

DuckDB expose une API C/C++ complète, mais l’appel depuis Windev via API Externe est laborieux :

  • Gestion manuelle de la mémoire : Allocation/désallocation de buffers, marshalling de pointeurs.
  • Marshalling de structures C : Les structures DuckDB (duckdb_result, duckdb_value) sont complexes à manipuler en WLangage.
  • Maintenance difficile : À chaque mise à jour de DuckDB, il faut vérifier la compatibilité des signatures de fonctions.
  • Code verbeux et sujet aux erreurs : Risque de fuites mémoire, erreurs de segmentation.

1.3 Pourquoi .NET/C# est la bonne approche

Un assemblage .NET C# résout tous ces problèmes :

  • Client .NET officiel : Package NuGet DuckDB.NET.Data maintenu et documenté.
  • Intégration native dans Windev : qui permet d’appeler directement les méthodes C# depuis WLangage.
  • Code managé : Pas de gestion manuelle de la mémoire, garbage collector automatique.
  • Typage fort : Les erreurs sont détectées à la compilation, moins de bugs en production.
  • Facilité de maintenance : Mise à jour du package NuGet, recompilation, c’est tout.

2. Architecture de la solution

2.1 Vue d’ensemble

┌─────────────────────┐
│ Windev Application  │
└──────────┬──────────┘
           │ (génère CSV)
           ↓
┌─────────────────────┐
│   Fichiers CSV      │  articles.csv, ventes.csv, prix_achat.csv
└──────────┬──────────┘
           │
           ↓
┌─────────────────────┐
│ Wrapper .NET C#     │ ← DuckDB.NET NuGet
└──────────┬──────────┘
           │
           ↓
┌─────────────────────┐
│  benchmark.duckdb   │
└──────────┬──────────┘
           │ (requêtes OLAP)
           ↓
┌─────────────────────┐
│  Résultats JSON     │ → consommés par Windev
└─────────────────────┘

2.2 Flux de données

  1. Windev exporte les données HFSQL → CSV : Utilisation de HExporteCSV() pour extraire les tables ou utilisation de fonctions d’export personnalisées.
  2. Wrapper .NET importe CSV → DuckDB : Commande COPY en bulk, très performante.
  3. Windev envoie des requêtes SQL → Wrapper : Appel de méthode C# avec la requête en paramètre.
  4. Wrapper exécute sur DuckDB → retour JSON : Résultats sérialisés en JSON pour faciliter la désérialisation Windev.
  5. Windev parse JSON → affichage tableaux de bord : JSONVersVariant() permet de consommer directement les résultats.

3. Implémentation du wrapper .NET

3.1 Configuration du projet C#

Dans Visual Studio (ou Visual Studio Code avec .NET SDK) :

  1. Créer un projet Class Library (.NET Framework 4.8 ou .NET 6+, selon la version de Windev).

  2. Installer le package NuGet DuckDB.NET.Data :

    dotnet add package DuckDB.NET.Data
    
  3. Ajouter une référence à System.Text.Json pour la sérialisation JSON (ou Newtonsoft.Json si préféré).

3.2 Classe DuckDBWrapper - Méthodes principales

Voici les trois méthodes essentielles du wrapper :

Méthode Connect(string dbPath)

using DuckDB.NET.Data;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json;

public class DuckDBWrapper
{
    private DuckDBConnection _connection;

    /// <summary>
    /// Connexion à une base DuckDB. Crée le fichier s'il n'existe pas.
    /// </summary>
    /// <param name="dbPath">Chemin absolu vers le fichier .duckdb</param>
    /// <returns>"OK" en cas de succès, "ERREUR: message" sinon</returns>
    public string Connect(string dbPath)
    {
        try
        {
            var connectionString = $"Data Source={dbPath}";
            _connection = new DuckDBConnection(connectionString);
            _connection.Open();
            return "OK";
        }
        catch (Exception ex)
        {
            return $"ERREUR: {ex.Message}";
        }
    }
}

Méthode ImportCsvWithCreateTable(string createTableSql, string csvPath, string tableName)

/// <summary>
/// Crée une table et importe un fichier CSV en bulk.
/// </summary>
/// <param name="createTableSql">Script SQL de CREATE TABLE</param>
/// <param name="csvPath">Chemin absolu vers le fichier CSV</param>
/// <param name="tableName">Nom de la table à alimenter</param>
/// <returns>"OK|durée_ms" en cas de succès, "ERREUR|message" sinon</returns>
public string ImportCsvWithCreateTable(string createTableSql, string csvPath, string tableName)
{
    var stopwatch = Stopwatch.StartNew();

    try
    {
        using (var cmd = _connection.CreateCommand())
        {
            // Création de la table
            cmd.CommandText = createTableSql;
            cmd.ExecuteNonQuery();

            // Import CSV avec COPY (normalisation des slashes pour DuckDB)
            cmd.CommandText = $@"
                COPY {tableName}
                FROM '{csvPath.Replace("\\", "/")}'
                WITH (HEADER TRUE, DELIMITER ';')";
            cmd.ExecuteNonQuery();
        }

        stopwatch.Stop();
        return $"OK|{stopwatch.ElapsedMilliseconds}ms";
    }
    catch (Exception ex)
    {
        stopwatch.Stop();
        return $"ERREUR|{ex.Message}";
    }
}

Méthode ExecuteQueryToJson(string sql)

/// <summary>
/// Exécute une requête SQL et retourne les résultats en JSON.
/// </summary>
/// <param name="sql">Requête SQL à exécuter</param>
/// <returns>JSON array en cas de succès, {"error": "message"} sinon</returns>
public string ExecuteQueryToJson(string sql)
{
    try
    {
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = sql;
            using (var reader = cmd.ExecuteReader())
            {
                var results = new List<Dictionary<string, object>>();

                while (reader.Read())
                {
                    var row = new Dictionary<string, object>();
                    for (int i = 0; i < reader.FieldCount; i++)
                    {
                        row[reader.GetName(i)] = reader.GetValue(i);
                    }
                    results.Add(row);
                }

                return JsonSerializer.Serialize(results);
            }
        }
    }
    catch (Exception ex)
    {
        return $"{{\"error\": \"{ex.Message}\"}}";
    }
}

3.3 Compilation et déploiement

  1. Build en Release : Compiler le projet en mode Release pour optimiser les performances.
  2. Copier les DLL :
    • DuckDBWrapper.dll (votre assemblage)
    • DuckDB.NET.Data.dll et ses dépendances (copiées automatiquement dans le dossier bin/Release)
  3. Placer dans le projet Windev : Créer un sous-dossier assemblages/ dans le répertoire du projet Windev et y copier toutes les DLL.

4. Utilisation depuis Windev

4.1 Déclaration de l’assemblage

Dans le code d’initialisation du projet Windev ou de la fenêtre :

// Déclaration de l'assemblage .NET
duckdb est un DuckDbWrapper

4.2 Génération des fichiers CSV

Pourquoi écrire une fonction d’export CSV personnalisée ?
Pour contrôler le format (délimiteur, encodage, format des dates AAAA-MM-JJ) et optimiser la vitesse d’écriture.

TableNombre de lignesExport csvHExporteCSV
Articles1 00092 ms2 s
Ventes10 000 0004 min 21s7 min 30s
Prix d’achat3 000 0001 min 40s1 min 50s

Procédure pour exporter les données HFSQL vers des fichiers CSV :

PROCÉDURE ExporterDonnéesVersCSV()
    sRepertoireExport est une chaine = "C:\temp"
    sContenu est une chaîne

    fSupprime(sRepertoireExport+"articles.csv",frLectureSeule)
    fSupprime(sRepertoireExport+"prix_achat.csv",frLectureSeule)
    fSupprime(sRepertoireExport+"ventes.csv",frLectureSeule)

    sContenu = "id_article;code_article;libelle;famille;sous_famille;is_actif"
    POUR TOUT articles
        sContenu += RC+ articles.id_article+";"+articles.code_article+";"+articles.libelle+";"+articles.famille+";"+articles.sous_famille+";"+articles.is_actif
    FIN
    fSauveTexte(sRepertoireExport+"articles.csv",sContenu)

    sContenu="id_prix_achat;id_article;id_fournisseur;date_debut;date_fin;prix_achat_ht"
    POUR TOUT prix_achat
        sContenu += RC+prix_achat.id_prix_achat+";"+prix_achat.id_article+";"+prix_achat.id_fournisseur+";"+DateVersChaîne(prix_achat.date_debut,"AAAA-MM-JJ")+";"+DateVersChaîne(prix_achat.date_fin,"AAAA-MM-JJ")+";"+prix_achat.prix_achat_ht
    FIN
    fSauveTexte(sRepertoireExport+"prix_achat.csv",sContenu)

    sContenu = "id_vente;date_vente;id_article;id_client;quantite;prix_vente_ht;remise_pct;id_magasin"
    POUR TOUT ventes
        sContenu += RC+ventes.id_vente+";"+DateVersChaîne(ventes.date_vente,"AAAA-MM-JJ")+";"+ventes.id_article+";"+ventes.id_client+";"+ventes.quantite+";"+ventes.prix_vente_ht+";"+ventes.remise_pct+";"+ventes.id_magasin
    FIN
    fSauveTexte(sRepertoireExport+"ventes.csv",sContenu)

    Info("Export CSV terminé")
FIN

4.3 Import dans DuckDB

Procédure pour créer les tables et importer les CSV dans DuckDB :

PROCÉDURE ImporterDansDuckDB()
    sCheminDB est une chaine = "C:\temp\benchmark.duckdb"

    // Connexion à la base DuckDB
   duckdb.Connect(sCheminDB)


    // Import Articles
    sSqlArticles est une chaine = [
        CREATE TABLE articles (
            id_article INTEGER,
            code_article VARCHAR,
            libelle VARCHAR,
            famille VARCHAR,
            sous_famille VARCHAR,
            is_actif BOOLEAN
        )
    ]

    duckdb.ImportCsvWithCreateTable(sSqlArticles, "C:\temp\articles.csv", "articles")

    // Import Ventes
    sSqlVentes est une chaine = [
        CREATE TABLE ventes (
            id_vente BIGINT,
            date_vente DATE,
            id_article INTEGER,
            id_client INTEGER,
            quantite INTEGER,
            prix_vente_ht DECIMAL(18,4),
            remise_pct DECIMAL(5,2),
            id_magasin INTEGER
        )
    ]

    duckdb.ImportCsvWithCreateTable(sSqlVentes, "C:\temp\ventes.csv", "ventes")

    // Import Prix d'achat
    sSqlPrixAchat est une chaine = [
        CREATE TABLE prix_achat (
            id_prix_achat BIGINT,
            id_article INTEGER,
            id_fournisseur INTEGER,
            date_debut DATE,
            date_fin DATE,
            prix_achat_ht DECIMAL(18,4)
        )
    ]

    duckdb.ImportCsvWithCreateTable(sSqlPrixAchat, "C:\temp\prix_achat.csv", "prix_achat")

    Info("Import DuckDB terminé avec succès")
FIN

4.4 Exécution de requêtes OLAP

Exemple : requête de chiffre d’affaires mensuel par famille d’articles :

PROCÉDURE RequêteCAMensuel()
    jsJson est un Json
    tabRésultats est un variant
    
    // Connexion
    duckdb.Connect("C:\temp\benchmark.duckdb")

    // Requête CA mensuel par famille
    sSql = [
        SELECT
            date_trunc('year', date_vente) AS annee,
            date_trunc('month', date_vente) AS mois,
            famille,
            SUM(quantite * prix_vente_ht * (1 - remise_pct / 100.0)) AS ca_ht
        FROM ventes
        JOIN articles USING(id_article)
        where date_vente >= '2024-01-01' AND date_vente < '2024-12-31'
        GROUP BY 1, 2, 3
        ORDER BY 1, 2, 3
    ]

    jsJson = duckdb.ExecuteQueryToJson(sSql)

    // Désérialisation JSON en Windev
    tabRésultats = JSONVersVariant(sJson)

    // Vider le champ table avant remplissage
    TableSupprimeTout(TABLE_CA)

    // Affichage dans un champ table
    POUR CHAQUE element DE tabRésultats
        TableAjouteLigne(TABLE_CA, element.annee, element.mois, element.famille, element.ca_ht)
    FIN
FIN

5. Optimisations et bonnes pratiques

5.1 Gestion de la connexion

Pour éviter d’ouvrir/fermer la connexion à chaque appel :

  • Singleton : Créer une instance unique du wrapper dans le projet Windev (variable globale).
  • Fermeture explicite : Ajouter une méthode Dispose() dans le wrapper C# pour libérer proprement la connexion.
  • Pool de connexions : Si multi-threading, envisager un pool simple.

5.2 Performance d’import CSV

Quelques astuces pour maximiser les performances :

  • Utiliser COPY plutôt que INSERT : Le bulk insert de DuckDB est extrêmement rapide.
  • Activer le parallélisme : Ajouter PRAGMA threads=4; (ou plus selon le CPU) avant l’import.
  • Éviter les transactions explicites : Pour l’import, auto-commit est plus rapide.

Exemple d’activation du parallélisme :

public string SetThreads(int threadCount)
{
    try
    {
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = $"PRAGMA threads={threadCount}";
            cmd.ExecuteNonQuery();
        }
        return "OK";
    }
    catch (Exception ex)
    {
        return $"ERREUR: {ex.Message}";
    }
}

5.3 Sécurité

Quelques précautions à prendre :

  • Paramétrage des requêtes : Si les requêtes contiennent des inputs utilisateur, utiliser des paramètres pour éviter les injections SQL.
  • Validation des chemins fichiers : Vérifier que les chemins CSV existent et sont dans un répertoire autorisé.
  • Gestion des exceptions : Toujours encapsuler les appels dans des try/catch et retourner des messages explicites.

Exemple de requête paramétrée :

public string ExecuteQueryToJson(string sql, Dictionary<string, object> parameters)
{
    try
    {
        using (var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = sql;

            foreach (var param in parameters)
            {
                var p = cmd.CreateParameter();
                p.ParameterName = param.Key;
                p.Value = param.Value;
                cmd.Parameters.Add(p);
            }

            // Exécution...
        }
    }
    catch (Exception ex)
    {
        return $"{{\"error\": \"{ex.Message}\"}}";
    }
}

6. Cas d’usage réels testés

6.1 Volumes traités

Le tableau ci-dessous présente les volumes de données testés et les temps d’import des fichiers csv mesurés :

TableNombre de lignesTemps d’importRemarques
Articles1 00026 msRéférentiel produit
Ventes10 000 0001 s 433 msHistorique 3 ans
Prix d’achat3 000 000438 msHistorique fournisseurs 3 ans

6.2 Performances des requêtes

Mesures effectuées sur les requêtes OLAP typiques (après plusieurs exécutions à chaud) :

Requête (sur l’année 2025)Temps d’exécutionRésultats retournés
CA mensuel par famille177 ms77 lignes
Marge brute par article et fournisseur48 s119 986 lignes
Top 10 clients par chiffre d’affaires322 ms10 lignes

Les 48 secondes pour la marge brute s’expliquent par le volume de données (10 millions de ventes jointes à 3 millions de prix d’achat) et les calculs effectués.
Cependant, c’est nettement plus rapide que ce qui serait possible avec HFSQL en OLTP. De plus, DuckDB peut être optimisé davantage avec des index ou des vues matérialisées si nécessaire.


7. Limitations et points d’attention

7.1 Limitations connues de DuckDB

  • Mono-utilisateur : Le fichier .duckdb est verrouillé pendant l’utilisation. Une seule connexion à la fois.
  • Pas de transactions ACID complexes : DuckDB est optimisé pour l’analytique, pas pour les transactions concurrentes.
  • Optimisé lecture : Les écritures en temps réel sont possibles mais moins performantes que PostgreSQL.

7.2 Quand utiliser cette approche

✅ Bon pour :

  • Tableaux de bord / rapports analytiques : Agrégations, calculs de marges, analyses temporelles.
  • Analyses ad-hoc sur gros volumes : Requêtes exploratoires sans impacter la base OLTP.
  • Exports de données pour BI : Préparation de cubes OLAP, datawarehouse léger.

❌ Pas pour :

  • Applications multi-utilisateurs concurrentes : Préférer PostgreSQL ou SQL Server.
  • Saisie transactionnelle en temps réel : HFSQL ou autre SGBD OLTP.
  • Remplacer HFSQL pour l’OLTP : DuckDB ne gère pas les transactions complexes.

8. Accès au code source

Le projet complet est disponible sur demande et inclut :

  • Projet Windev exemple avec procédures d’export, import et requêtes,
  • Wrapper DuckDB .NET C# complet,

Conclusion

L’intégration de DuckDB dans Windev via un wrapper .NET C# apporte de nombreux bénéfices :

  • Simplicité : API .NET propre et maintenable, pas de gestion mémoire manuelle.
  • Performance : Gains significatifs (10-20x) sur les requêtes analytiques par rapport à HFSQL en OLTP.
  • Fiabilité : Gestion d’erreurs robuste, typage fort, moins de bugs.
  • Évolutivité : Facile d’ajouter de nouvelles méthodes (gestion des paramètres, nouvelles fonctionnalités DuckDB).

Cette approche permet aux développeurs Windev de bénéficier de la puissance OLAP de DuckDB sans complexité technique, avec une intégration native dans l’écosystème PC Soft.

Pour aller plus loin :

  • Prochainement : “Automatiser les rafraîchissements DuckDB avec le planificateur Windev”

Si vous avez des questions ou souhaitez partager votre expérience, n’hésitez pas à ouvrir une issue sur le dépôt GitHub !