DuckDB.NET

repository·develop·Indexed 20 days ago

https://github.com/giorgi/duckdb.net

C# bindings for the DuckDB analytical database providing an ADO.NET compatible interface. It enables executing SQL queries via DuckDBConnection and offers a type-safe DuckDBAppenderMap<T> pattern to validate .NET types against the database schema to prevent data corruption. Supports connections to local database files and MotherDuck.

Tokens
1.8K
Snippets
5
Records
9
Agent score
22%

What's inside DuckDB.NET

  1. Implement a type-safe AppenderMap for DuckDB

    develop

    To prevent silent data corruption caused by type mismatches (e.g., appending a decimal to a REAL column), use the DuckDBAppenderMap<T> pattern. This approach validates your .NET types against the actual database schema at the moment the appender is created.

    1. Define the Map

    Create a class inheriting from DuckDBAppenderMap<T>. You must define property mappings in the exact order they appear in the database table columns.

    public class PersonMap : DuckDBAppenderMap<Person>
    {
        public PersonMap()
        {
            Map(p => p.Id);            // Column 0
            Map(p => p.Name);          // Column 1
            Map(p => p.Height);        // Column 2
            Map(p => p.BirthDate);     // Column 3
        }
    }

    2. Use the Type-Safe Appender

    Use the CreateAppender<T, TMap> method on your connection. If the types in your map do not match the database schema, an exception will be thrown immediately upon creation.

    // Create table
    connection.ExecuteNonQuery("CREATE TABLE person(id INTEGER, name VARCHAR, height REAL, birth_date TIMESTAMP)");
    
    var people = new[] 
    {
        new Person { Id = 1, Name = "Alice", Height = 1.65f, BirthDate = new DateTime(1990, 1, 15) }
    };
    
    // Type validation happens here at creation
    using (var appender = connection.CreateAppender<Person, PersonMap>("person"))
    {
        appender.AppendRecords(people);
    }
    public class PersonMap : DuckDBAppenderMap<Person>
    {
        public PersonMap()
        {
            Map(p => p.Id);
            Map(p => p.Name);
            Map(p => p.Height);
            Map(p => p.BirthDate);
        }
    }
    
    using (var appender = connection.CreateAppender<Person, PersonMap>("person"))
    {
        appender.AppendRecords(people);
    }
  2. Connect to MotherDuck

    develop

    To connect to MotherDuck, use a connection string starting with md: and include your database name and motherduck_token.

    using var duckDBConnection = new DuckDBConnection("DataSource=md:{your_database}?motherduck_token=ey...");
  3. Install DuckDB.NET

    develop

    You can install DuckDB.NET via NuGet. The DuckDB.NET.Data.Full package is recommended for most users as it includes the necessary native dependencies.

    Use the following command to add the full package to your project:

    dotnet add package DuckDB.NET.Data.Full
  4. Troubleshoot System.AccessViolationException during debugging

    develop

    If you encounter System.AccessViolationException: Attempted to read or write protected memory while debugging, this is typically caused by the debugger interacting with native memory during marshalling.

    Workaround: Check the JetBrains Rider issue regarding debugger options messing up debugging sessions during marshalling for specific configuration steps to resolve this.

  5. Basic usage of DuckDB.NET

    develop

    DuckDB.NET provides an ADO.NET compatible interface. You can use DuckDBConnection to connect to a database file, create commands, and execute queries using standard methods like ExecuteNonQuery, ExecuteScalar, and ExecuteReader.

    using (var duckDBConnection = new DuckDBConnection("Data Source=file.db"))
    {
      duckDBConnection.Open();
    
      using var command = duckDBConnection.CreateCommand();
    
      command.CommandText = "CREATE TABLE integers(foo INTEGER, bar INTEGER);";
      var executeNonQuery = command.ExecuteNonQuery();
    
      command.CommandText = "INSERT INTO integers VALUES (3, 4), (5, 6), (7, 8);";
      executeNonQuery = command.ExecuteNonQuery();
    
      command.CommandText = "Select count(*) from integers";
      var executeScalar = command.ExecuteScalar();
    
      command.CommandText = "SELECT foo, bar FROM integers";
      var reader = command.ExecuteReader();
    
      PrintQueryResults(reader);
    }
    
    private static void PrintQueryResults(DbDataReader queryResult)
    {
      for (var index = 0; index < queryResult.FieldCount; index++)
      {
        var column = queryResult.GetName(index);
        Console.Write($"{column} ");
      }
    
      Console.WriteLine();
    
      while (queryResult.Read())
      {
        for (int ordinal = 0; ordinal < queryResult.FieldCount; ordinal++)
        {
          var val = queryResult.GetInt32(ordinal);
          Console.Write(val);
          Console.Write(" ");
        }
    
        Console.WriteLine();
      }
    }
  6. Create Appender with different scope levels

    develop

    The CreateAppender<T, TMap> method supports different levels of database object scoping:

    • Table only: connection.CreateAppender<T, TMap>("tableName")
    • Schema and Table: connection.CreateAppender<T, TMap>("schemaName", "tableName")
    • Catalog, Schema, and Table: connection.CreateAppender<T, TMap>("catalog", "schema", "table")
  7. Configure AppenderMap mapping options

    develop

    When defining a DuckDBAppenderMap<T>, you can control how specific columns are handled using the following methods within the constructor:

    • Map(expression): Maps a property to the next available column in the sequence.
    • DefaultValue(): Instructs the appender to use the column's defined default value instead of a value from the object.
    • NullValue(): Instructs the appender to insert a NULL value for that column.

    Example:

    public class MyMap : DuckDBAppenderMap<MyData>
    {
        public MyMap()
        {
            Map(d => d.Id);
            Map(d => d.Name);
            DefaultValue();  // Use column's default value
            NullValue();     // Insert NULL
        }
    }
    public class MyMap : DuckDBAppenderMap<MyData>
    {
        public MyMap()
        {
            Map(d => d.Id);
            Map(d => d.Name);
            DefaultValue();
            NullValue();
        }
    }
  8. Reference .NET to DuckDB type mappings

    develop

    The AppenderMap validates .NET types against the following DuckDB types. If a mismatch occurs (e.g., using decimal for a REAL column), an exception is thrown during appender creation.

    .NET TypeDuckDB Type
    boolBoolean
    sbyteTinyInt
    shortSmallInt
    intInteger
    longBigInt
    byteUnsignedTinyInt
    ushortUnsignedSmallInt
    uintUnsignedInteger
    ulongUnsignedBigInt
    floatFloat
    doubleDouble
    decimalDecimal
    stringVarchar
    DateTimeTimestamp
    DateTimeOffsetTimestampTz
    TimeSpanInterval
    GuidUuid
    DateOnlyDate
    TimeOnlyTime