Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Testcontainers.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<Project Path="src/Testcontainers.Couchbase/Testcontainers.Couchbase.csproj"/>
<Project Path="src/Testcontainers.CouchDb/Testcontainers.CouchDb.csproj"/>
<Project Path="src/Testcontainers.Db2/Testcontainers.Db2.csproj"/>
<Project Path="src/Testcontainers.DuckDb/Testcontainers.DuckDb.csproj"/>
<Project Path="src/Testcontainers.DynamoDb/Testcontainers.DynamoDb.csproj"/>
<Project Path="src/Testcontainers.Elasticsearch/Testcontainers.Elasticsearch.csproj"/>
<Project Path="src/Testcontainers.EventHubs/Testcontainers.EventHubs.csproj"/>
Expand Down Expand Up @@ -100,6 +101,7 @@
<Project Path="tests/Testcontainers.CouchDb.Tests/Testcontainers.CouchDb.Tests.csproj"/>
<Project Path="tests/Testcontainers.Databases.Tests/Testcontainers.Databases.Tests.csproj"/>
<Project Path="tests/Testcontainers.Db2.Tests/Testcontainers.Db2.Tests.csproj"/>
<Project Path="tests/Testcontainers.DuckDb.Tests/Testcontainers.DuckDb.Tests.csproj"/>
<Project Path="tests/Testcontainers.DynamoDb.Tests/Testcontainers.DynamoDb.Tests.csproj"/>
<Project Path="tests/Testcontainers.Elasticsearch.Tests/Testcontainers.Elasticsearch.Tests.csproj"/>
<Project Path="tests/Testcontainers.EventHubs.Tests/Testcontainers.EventHubs.Tests.csproj"/>
Expand Down
39 changes: 39 additions & 0 deletions docs/modules/duckdb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# DuckDB

[DuckDB](https://duckdb.org/) is a fast, in-process analytical database. It runs embedded within a host process and stores data in a single database file.

Add the following dependency to your project file:

```shell title="NuGet"
dotnet add package Testcontainers.DuckDb
```

You can start a DuckDB container instance from any .NET application. This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the container. The container is started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the `DisposeAsync` method.

=== "Test class"
```csharp
--8<-- "tests/Testcontainers.DuckDb.Tests/DuckDbContainerTest.docs.cs:UseDuckDbContainer"
}
```

Execute a SQL script:

=== "Run SQL script"
```csharp
--8<-- "tests/Testcontainers.DuckDb.Tests/DuckDbContainerTest.docs.cs:RunSQLScript"
```

!!! note

DuckDB is an embedded database — the [duckdb/duckdb](https://hub.docker.com/r/duckdb/duckdb) image ships the DuckDB CLI, not a database server. The container keeps an in-memory DuckDB CLI process running to stay alive, and `ExecScriptAsync(string)` runs SQL scripts against the configured database file (default: `/database.duckdb`, created on first use). The database state persists across script executions. Use `WithDatabase(string)` to configure a different database file path, and `GetDatabaseFilePath()` together with `ReadFileAsync(string)` to copy the database file to the test host.

The test example uses the following NuGet dependencies:

=== "Package References"
```xml
--8<-- "tests/Testcontainers.DuckDb.Tests/Testcontainers.DuckDb.Tests.csproj:PackageReferences"
```

To execute the tests, use the command `dotnet test` from a terminal.

--8<-- "docs/modules/_call_out_test_projects.txt"
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ nav:
- modules/servicebus.md # Azure
- modules/clickhouse.md
- modules/db2.md
- modules/duckdb.md
- modules/elasticsearch.md
- modules/garnet.md
- modules/grafana.md
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.DuckDb/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
134 changes: 134 additions & 0 deletions src/Testcontainers.DuckDb/DuckDbBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
namespace Testcontainers.DuckDb;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class DuckDbBuilder : ContainerBuilder<DuckDbBuilder, DuckDbContainer, DuckDbConfiguration>
{
[Obsolete("This constant is obsolete and will be removed in the future. Use the constructor with the image parameter instead: https://github.com/testcontainers/testcontainers-dotnet/discussions/1470#discussioncomment-15185721.")]
public const string DuckDbImage = "duckdb/duckdb:1.5.5";

/// <summary>
/// The path of the DuckDB CLI binary inside the container.
/// </summary>
public const string DuckDbBinaryFilePath = "/duckdb";

/// <summary>
/// The default path of the DuckDB database file inside the container.
/// </summary>
public const string DefaultDatabaseFilePath = "/database.duckdb";

/// <summary>
/// Initializes a new instance of the <see cref="DuckDbBuilder" /> class.
/// </summary>
[Obsolete("This parameterless constructor is obsolete and will be removed. Use the constructor with the image parameter instead: https://github.com/testcontainers/testcontainers-dotnet/discussions/1470#discussioncomment-15185721.")]
[ExcludeFromCodeCoverage]
public DuckDbBuilder()
: this(DuckDbImage)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DuckDbBuilder" /> class.
/// </summary>
/// <param name="image">
/// The full Docker image name, including the image repository and tag
/// (e.g., <c>duckdb/duckdb:1.5.5</c>).
/// </param>
/// <remarks>
/// Docker image tags available at <see href="https://hub.docker.com/r/duckdb/duckdb/tags" />.
/// </remarks>
public DuckDbBuilder(string image)
: this(new DockerImage(image))
{
}

/// <summary>
/// Initializes a new instance of the <see cref="DuckDbBuilder" /> class.
/// </summary>
/// <param name="image">
/// An <see cref="IImage" /> instance that specifies the Docker image to be used
/// for the container builder configuration.
/// </param>
/// <remarks>
/// Docker image tags available at <see href="https://hub.docker.com/r/duckdb/duckdb/tags" />.
/// </remarks>
public DuckDbBuilder(IImage image)
: this(new DuckDbConfiguration())
{
DockerResourceConfiguration = Init().WithImage(image).DockerResourceConfiguration;
}

/// <summary>
/// Initializes a new instance of the <see cref="DuckDbBuilder" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
private DuckDbBuilder(DuckDbConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
DockerResourceConfiguration = resourceConfiguration;
}

/// <inheritdoc />
protected override DuckDbConfiguration DockerResourceConfiguration { get; }

/// <summary>
/// Sets the path of the DuckDB database file inside the container.
/// </summary>
/// <remarks>
/// DuckDB is an embedded database. The container keeps an in-memory DuckDB CLI
/// process running to stay alive; SQL scripts run against the database file
/// (created on first use) using <see cref="DuckDbContainer.ExecScriptAsync" />.
/// </remarks>
/// <param name="database">The DuckDB database file path.</param>
/// <returns>A configured instance of <see cref="DuckDbBuilder" />.</returns>
public DuckDbBuilder WithDatabase(string database)
{
return Merge(DockerResourceConfiguration, new DuckDbConfiguration(database: database));
}

/// <inheritdoc />
public override DuckDbContainer Build()
{
Validate();
return new DuckDbContainer(DockerResourceConfiguration);
}

/// <inheritdoc />
protected override DuckDbBuilder Init()
{
return base.Init()
.WithEntrypoint(DuckDbBinaryFilePath)
.WithCommand("-cmd", "SELECT 1;")
.WithCreateParameterModifier(parameterModifier => parameterModifier.OpenStdin = true)
.WithDatabase(DefaultDatabaseFilePath)
.WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted(DuckDbBinaryFilePath, "-c", "SELECT 1;"));
}

/// <inheritdoc />
protected override void Validate()
{
base.Validate();

_ = Guard.Argument(DockerResourceConfiguration.Database, nameof(DockerResourceConfiguration.Database))
.NotNull()
.NotEmpty();
}

/// <inheritdoc />
protected override DuckDbBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new DuckDbConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override DuckDbBuilder Clone(IContainerConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new DuckDbConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override DuckDbBuilder Merge(DuckDbConfiguration oldValue, DuckDbConfiguration newValue)
{
return new DuckDbBuilder(new DuckDbConfiguration(oldValue, newValue));
}
}
62 changes: 62 additions & 0 deletions src/Testcontainers.DuckDb/DuckDbConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Testcontainers.DuckDb;

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class DuckDbConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="DuckDbConfiguration" /> class.
/// </summary>
/// <param name="database">The DuckDB database file path.</param>
public DuckDbConfiguration(
string database = null)
{
Database = database;
}

/// <summary>
/// Initializes a new instance of the <see cref="DuckDbConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public DuckDbConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="DuckDbConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public DuckDbConfiguration(IContainerConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="DuckDbConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public DuckDbConfiguration(DuckDbConfiguration resourceConfiguration)
: this(new DuckDbConfiguration(), resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="DuckDbConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
public DuckDbConfiguration(DuckDbConfiguration oldValue, DuckDbConfiguration newValue)
: base(oldValue, newValue)
{
Database = BuildConfiguration.Combine(oldValue.Database, newValue.Database);
}

/// <summary>
/// Gets the DuckDB database file path.
/// </summary>
public string Database { get; }
}
66 changes: 66 additions & 0 deletions src/Testcontainers.DuckDb/DuckDbContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace Testcontainers.DuckDb;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public sealed class DuckDbContainer : DockerContainer
{
private readonly SemaphoreSlim _scriptExecutionSemaphore = new SemaphoreSlim(1, 1);

private readonly DuckDbConfiguration _configuration;

/// <summary>
/// Initializes a new instance of the <see cref="DuckDbContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
public DuckDbContainer(DuckDbConfiguration configuration)
: base(configuration)
{
_configuration = configuration;
}

/// <summary>
/// Gets the path of the DuckDB database file inside the container.
/// </summary>
/// <remarks>
/// DuckDB is an embedded database. The database file is created on first use;
/// copy it out of the container with <see cref="DockerContainer.ReadFileAsync" />
/// to use it with a DuckDB client library on the test host.
/// </remarks>
/// <returns>The DuckDB database file path.</returns>
public string GetDatabaseFilePath()
{
return _configuration.Database;
}

/// <summary>
/// Executes the SQL script in the DuckDB container.
/// </summary>
/// <remarks>
/// Each execution runs a dedicated DuckDB process against the database file. DuckDB
/// does not support concurrent write access to the same database file from multiple
/// processes, therefore script executions are serialized per container instance.
/// </remarks>
/// <param name="scriptContent">The content of the SQL script to execute.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the SQL script has been executed.</returns>
public async Task<ExecResult> ExecScriptAsync(string scriptContent, CancellationToken ct = default)
{
var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName());

await _scriptExecutionSemaphore.WaitAsync(ct)
.ConfigureAwait(false);

try
{
await CopyAsync(Encoding.UTF8.GetBytes(scriptContent), scriptFilePath, fileMode: Unix.FileMode644, ct: ct)
.ConfigureAwait(false);

return await ExecAsync(new[] { DuckDbBuilder.DuckDbBinaryFilePath, _configuration.Database, "-f", scriptFilePath }, ct)
.ConfigureAwait(false);
}
finally
{
_scriptExecutionSemaphore.Release();
}
}
}
12 changes: 12 additions & 0 deletions src/Testcontainers.DuckDb/Testcontainers.DuckDb.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" VersionOverride="2026.2.0" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Testcontainers/Testcontainers.csproj"/>
</ItemGroup>
</Project>
13 changes: 13 additions & 0 deletions src/Testcontainers.DuckDb/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
global using System;
global using System.Diagnostics.CodeAnalysis;
global using System.IO;
global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;
global using Docker.DotNet.Models;
global using DotNet.Testcontainers;
global using DotNet.Testcontainers.Builders;
global using DotNet.Testcontainers.Configurations;
global using DotNet.Testcontainers.Containers;
global using DotNet.Testcontainers.Images;
global using JetBrains.Annotations;
1 change: 1 addition & 0 deletions tests/Testcontainers.DuckDb.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
1 change: 1 addition & 0 deletions tests/Testcontainers.DuckDb.Tests/.runs-on
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ubuntu-24.04
1 change: 1 addition & 0 deletions tests/Testcontainers.DuckDb.Tests/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM duckdb/duckdb:1.5.5@sha256:d17f30055ff2eeb7f45c7ac2b7e574542b91cfc7f41b73c049b99e40f5dc5b73
Loading
Loading