From 2905bb9c265f7f72fdcbd51e23d1613751b1064a Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 29 Sep 2026 02:53:41 +0400 Subject: [PATCH 01/26] Remove nullable annotations from appender tests The test project does not enable nullable reference types, so the reference-type annotations in the managed appender tests produced 18 CS8632 warnings. Without a nullable context they have no effect, so removing them changes nothing; the int? value types stay. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Test/DuckDBManagedAppenderListTests.cs | 14 +++++++------- DuckDB.NET.Test/DuckDBManagedAppenderTests.cs | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/DuckDB.NET.Test/DuckDBManagedAppenderListTests.cs b/DuckDB.NET.Test/DuckDBManagedAppenderListTests.cs index d014ed5b..49b01074 100644 --- a/DuckDB.NET.Test/DuckDBManagedAppenderListTests.cs +++ b/DuckDB.NET.Test/DuckDBManagedAppenderListTests.cs @@ -222,7 +222,7 @@ public void VarcharArrayValuesAfterNullArrayStayAligned() [Fact] public void ListOfArraysAfterNullArrayStayAligned() { - VerifyArrayRowsAfterNull?>("managedAppenderListOfArraysAfterNull", "INTEGER[2][]", + VerifyArrayRowsAfterNull>("managedAppenderListOfArraysAfterNull", "INTEGER[2][]", [[[1, 2], [3, 4]], [[5, 6], null, [7, 8]], [[9, 10]]]); } @@ -232,7 +232,7 @@ public void ListOfArraysAfterNullArrayStayAligned() [Fact] public void ArrayOfListsAfterNullArrayStayAligned() { - VerifyArrayRowsAfterNull?>("managedAppenderArrayOfListsAfterNull", "INTEGER[][2]", + VerifyArrayRowsAfterNull>("managedAppenderArrayOfListsAfterNull", "INTEGER[][2]", [[[1], [2, 3]], null, [[4], [5]], [[6, 7], [8]], [[9], null]]); } @@ -253,7 +253,7 @@ public void ArrayValuesWithNullItemsAfterNullArrayStayAligned() [Fact] public void VarcharArrayValuesWithNullItemsAfterNullArrayStayAligned() { - VerifyArrayRowsAfterNull("managedAppenderVarcharArrayNullItemsAfterNull", "VARCHAR[2]", + VerifyArrayRowsAfterNull("managedAppenderVarcharArrayNullItemsAfterNull", "VARCHAR[2]", [["a", null], null, [null, "d"], ["e", "f"]]); } @@ -283,7 +283,7 @@ public void ListOfArraysKeepValuesAfterListGrows() var batchSize = (int)DuckDBGlobalData.VectorSize; // Three arrays per row take the list past its initial VectorSize items a third of the way in. - var rows = Enumerable.Range(0, batchSize).Select(List?>? (i) => i switch + var rows = Enumerable.Range(0, batchSize).Select(List> (i) => i switch { 1000 => null, 1500 => [[i * 10, null], null, [i * 10 + 2, -(i * 10 + 2)]], @@ -301,7 +301,7 @@ public void ListOfNestedArraysKeepValuesAfterListGrows() var batchSize = (int)DuckDBGlobalData.VectorSize; // Three items per row take the list past its initial VectorSize items a third of the way in. - var rows = Enumerable.Range(0, batchSize).Select(List?>?>? (i) => + var rows = Enumerable.Range(0, batchSize).Select(List>> (i) => { var x = i * 10; @@ -324,7 +324,7 @@ public void ListOfArraysOfListsKeepValuesAfterListGrows() var batchSize = (int)DuckDBGlobalData.VectorSize; // Three items per row take the outer list past its initial VectorSize items a third of the way in. - var rows = Enumerable.Range(0, batchSize).Select(List?>?>? (i) => + var rows = Enumerable.Range(0, batchSize).Select(List>> (i) => { var x = i * 10; @@ -339,7 +339,7 @@ public void ListOfArraysOfListsKeepValuesAfterListGrows() VerifyArrayRowsAfterNull("managedAppenderListOfArraysOfListsAfterGrowth", "INTEGER[][2][]", rows); } - private void VerifyArrayRowsAfterNull(string table, string columnType, List?[] rows) + private void VerifyArrayRowsAfterNull(string table, string columnType, List[] rows) { Command.CommandText = $"CREATE TABLE {table} (a INTEGER, b {columnType});"; Command.ExecuteNonQuery(); diff --git a/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs b/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs index 4def6ec7..31482839 100644 --- a/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs +++ b/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs @@ -935,7 +935,7 @@ public void CollectionFinishedAfterFailedWriteHasNoNullItems(string columnType) using (var appender = Connection.CreateAppender(table)) { var row = appender.CreateRow(); - row.Invoking(r => r.AppendValue(new List { null, "bad" })).Should().Throw(); + row.Invoking(r => r.AppendValue(new List { null, "bad" })).Should().Throw(); row.AppendValue(new List { 1, 2 }).AppendValue("retried").EndRow(); } @@ -961,7 +961,7 @@ public void NestedCollectionFinishedAfterFailedWriteHasNoNullItems(string column using (var appender = Connection.CreateAppender(table)) { var row = appender.CreateRow(); - row.Invoking(r => r.AppendValue(new List { null, "bad" })).Should().Throw(); + row.Invoking(r => r.AppendValue(new List { null, "bad" })).Should().Throw(); row.AppendValue(new List> { new() { 1, 2 }, new() { 3, 4 } }).AppendValue("retried").EndRow(); } @@ -988,7 +988,7 @@ public void ListFinishedAfterFailedWriteHasNoNullItemsAfterListGrows() using (var appender = Connection.CreateAppender("managedAppenderListRetryGrows")) { var row = appender.CreateRow(); - row.Invoking(r => r.AppendValue(new List { null, "bad" })).Should().Throw(); + row.Invoking(r => r.AppendValue(new List { null, "bad" })).Should().Throw(); row.AppendValue(items).AppendValue("retried").EndRow(); } @@ -1013,7 +1013,7 @@ public void ListOfArraysFinishedAfterFailedWriteHasNoNullItemsAfterListGrows() using (var appender = Connection.CreateAppender("managedAppenderListOfArraysRetryGrows")) { var row = appender.CreateRow(); - row.Invoking(r => r.AppendValue(new List { null, "bad" })).Should().Throw(); + row.Invoking(r => r.AppendValue(new List { null, "bad" })).Should().Throw(); row.AppendValue(items).AppendValue("retried").EndRow(); } @@ -1188,7 +1188,7 @@ public void IncompleteRowWithPartialListIsNotWrittenOnDispose() appender.CreateRow().AppendValue(new List { 1 }).AppendValue("completed").EndRow(); var row = appender.CreateRow(); - row.Invoking(r => r.AppendValue(new List { 5, "bad" })).Should().Throw(); + row.Invoking(r => r.AppendValue(new List { 5, "bad" })).Should().Throw(); } Command.CommandText = "SELECT a, b FROM managedAppenderIncompleteList"; @@ -1210,7 +1210,7 @@ public void IncompleteRowWithPartialArrayIsNotWrittenOnDispose() appender.CreateRow().AppendValue(new List { 1, 2 }).AppendValue("completed").EndRow(); var row = appender.CreateRow(); - row.Invoking(r => r.AppendValue(new List { null, "bad" })).Should().Throw(); + row.Invoking(r => r.AppendValue(new List { null, "bad" })).Should().Throw(); } Command.CommandText = "SELECT a, b FROM managedAppenderIncompleteArray"; From f1fa31fb19940bb12f6e4e56f8aa08ec4c8ddd35 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 29 Sep 2026 03:02:48 +0400 Subject: [PATCH 02/26] Fix nullable and span comparison warnings - DuckDBConnectionStringBuilder.TryGetValue now matches the base signature ([NotNullWhen(true)] out object? value); the base method assigns null when the key is missing (CS8601). - DuckDBAppenderRow.AppendSpan checks for a span with no memory using Unsafe.IsNullRef instead of comparing it to null (CA2265). IsEmpty, as the analyzer suggests, would be wrong: a null byte[] or a default span writes NULL, but an empty array is an empty BLOB. A new test covers both cases. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Data/DuckDBAppenderRow.cs | 6 +++++- .../DuckDBConnectionStringBuilder.cs | 2 +- DuckDB.NET.Test/DuckDBManagedAppenderTests.cs | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/DuckDB.NET.Data/DuckDBAppenderRow.cs b/DuckDB.NET.Data/DuckDBAppenderRow.cs index 2d8cfedb..84b20f24 100644 --- a/DuckDB.NET.Data/DuckDBAppenderRow.cs +++ b/DuckDB.NET.Data/DuckDBAppenderRow.cs @@ -1,4 +1,6 @@ using DuckDB.NET.Data.DataChunk.Writer; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace DuckDB.NET.Data; @@ -194,7 +196,9 @@ private DuckDBAppenderRow AppendValueInternalClass(T? value) where T : class private unsafe IDuckDBAppenderRow AppendSpan(Span val) { - if (val == null) + // A null byte[] or a default span has no memory at all and writes NULL. An empty array still has memory and is + // an empty BLOB, so IsEmpty would be wrong here. + if (Unsafe.IsNullRef(ref MemoryMarshal.GetReference(val))) { return AppendNullValue(); } diff --git a/DuckDB.NET.Data/DuckDBConnectionStringBuilder.cs b/DuckDB.NET.Data/DuckDBConnectionStringBuilder.cs index 2f603e48..aa3e8aa8 100644 --- a/DuckDB.NET.Data/DuckDBConnectionStringBuilder.cs +++ b/DuckDB.NET.Data/DuckDBConnectionStringBuilder.cs @@ -180,7 +180,7 @@ public string DataSource public override bool ContainsKey(string keyword) => base.ContainsKey(Normalize(keyword)); /// - public override bool TryGetValue(string keyword, out object value) => base.TryGetValue(Normalize(keyword), out value); + public override bool TryGetValue(string keyword, [NotNullWhen(true)] out object? value) => base.TryGetValue(Normalize(keyword), out value); /// public override bool Remove(string keyword) => base.Remove(Normalize(keyword)); diff --git a/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs b/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs index 31482839..0e68a29e 100644 --- a/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs +++ b/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs @@ -146,6 +146,25 @@ public void ByteArray() dataReader.IsDBNull(0).Should().BeTrue(); } + [Fact] + public void EmptyByteArrayIsEmptyBlobAndDefaultSpanIsNull() + { + Command.CommandText = "CREATE TABLE blobAppenderEmptyTest(a Integer, b blob)"; + Command.ExecuteNonQuery(); + + using (var appender = Connection.CreateAppender("blobAppenderEmptyTest")) + { + appender.CreateRow().AppendValue(1).AppendValue(Array.Empty()).EndRow(); + appender.CreateRow().AppendValue(2).AppendValue(new Span(Array.Empty())).EndRow(); + appender.CreateRow().AppendValue(3).AppendValue(default(Span)).EndRow(); + } + + var results = Connection.Query<(bool IsNull, long? Length)>( + "SELECT b IS NULL, octet_length(b) FROM blobAppenderEmptyTest ORDER BY a").ToList(); + + results.Should().Equal((false, 0L), (false, 0L), (true, null)); + } + [Fact] public void Decimals() { From 90a1bf24306a1f24681e739c2b0dcd6f26c6d59f Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 29 Sep 2026 11:22:26 +0400 Subject: [PATCH 03/26] Order the empty BLOB test by rowid The test ordered its rows by an INTEGER column filled with AppendValue(1). An integer literal binds to the byte? overload, so the low-level appender wrote one byte into each INTEGER slot and left the rest uninitialized; ORDER BY then sorted on garbage and the test failed at random. A (int) cast does not help because the value is still a constant; (int?) or a variable would. The test now has only the BLOB column and orders by rowid. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Test/DuckDBManagedAppenderTests.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs b/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs index 0e68a29e..dc02d851 100644 --- a/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs +++ b/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs @@ -149,18 +149,20 @@ public void ByteArray() [Fact] public void EmptyByteArrayIsEmptyBlobAndDefaultSpanIsNull() { - Command.CommandText = "CREATE TABLE blobAppenderEmptyTest(a Integer, b blob)"; + // Only a BLOB column, ordered by rowid: an integer literal such as AppendValue(1) binds to the byte? overload, + // which the low-level appender would write into one byte of an INTEGER slot. + Command.CommandText = "CREATE TABLE blobAppenderEmptyTest(b blob)"; Command.ExecuteNonQuery(); using (var appender = Connection.CreateAppender("blobAppenderEmptyTest")) { - appender.CreateRow().AppendValue(1).AppendValue(Array.Empty()).EndRow(); - appender.CreateRow().AppendValue(2).AppendValue(new Span(Array.Empty())).EndRow(); - appender.CreateRow().AppendValue(3).AppendValue(default(Span)).EndRow(); + appender.CreateRow().AppendValue(Array.Empty()).EndRow(); + appender.CreateRow().AppendValue(new Span(Array.Empty())).EndRow(); + appender.CreateRow().AppendValue(default(Span)).EndRow(); } var results = Connection.Query<(bool IsNull, long? Length)>( - "SELECT b IS NULL, octet_length(b) FROM blobAppenderEmptyTest ORDER BY a").ToList(); + "SELECT b IS NULL, octet_length(b) FROM blobAppenderEmptyTest ORDER BY rowid").ToList(); results.Should().Equal((false, 0L), (false, 0L), (true, null)); } From c4bb7a91cc98323507b6905184e0a72f50019236 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 29 Sep 2026 11:34:16 +0400 Subject: [PATCH 04/26] Add AGENTS.md --- AGENTS.md | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..23b6b8bf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,171 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +DuckDB.NET is an ADO.NET provider and low-level bindings for DuckDB. The solution (`DuckDB.NET.slnx`) has five +projects: `Bindings` (P/Invoke over the DuckDB C API), `Data` (the ADO.NET provider), `Test` (xUnit), +`Samples` and `Benchmarks` (BenchmarkDotNet). The libraries target `net8.0;net10.0`; the test project targets +**net8.0 only**, so `dotnet test -f net10.0` silently runs nothing. The SDK is pinned by `global.json` (10.0.2xx). +User documentation lives in a separate docfx repository (`S:\src\DuckDB.NET-Docs`, published at https://duckdb.net). + +## Commands + +```bash +# Build everything the way CI does (Full = download native libraries for all platforms) +dotnet build --configuration Release /p:BuildType=Full + +# Run all tests (CI also adds /p:CollectCoverage=true /p:CoverletOutputFormat=lcov) +dotnet test DuckDB.NET.Test/Test.csproj --configuration Release /p:BuildType=Full + +# One class, one test, or everything except the slow VARINT parameter tests +dotnet test DuckDB.NET.Test/Test.csproj --filter "FullyQualifiedName~DuckDB.NET.Test.TableFunctionTests" +dotnet test DuckDB.NET.Test/Test.csproj --filter "FullyQualifiedName~DuckDB.NET.Test.TableFunctionTests.TestTableFunction" +dotnet test DuckDB.NET.Test/Test.csproj --filter "FullyQualifiedName!~VarintTest" + +# Packages: the plain packages need a separately installed native library; /p:BuildType=Full produces the .Full variants +dotnet pack DuckDB.NET.Data/Data.csproj --configuration Release /p:BuildType=Full + +# Benchmarks +dotnet run -c Release --project DuckDB.NET.Benchmarks +``` + +### Native libraries + +- The `DownloadNativeLibs` target in `Bindings.csproj` (with `DownloadNativeLibs.targets`) downloads the DuckDB + release zips into `DuckDB.NET.Bindings/obj/runtimes//native`. The test project always triggers this download, + whatever `BuildType` is. +- **The cache is version-blind:** a platform is downloaded only if its folder is missing. After changing the DuckDB + version, delete `DuckDB.NET.Bindings/obj/runtimes`, rebuild, and confirm the version with `SELECT version()`. +- **Nightly builds:** `/p:NightlyBuild=true /p:DuckDbArtifactRoot=https://artifacts.duckdb.org/latest` downloads + DuckDB's `duckdb-shared-libs-.tar.gz` instead. CI does this on the schedule, against the + `nightly-builds` branch. + +## DuckDB version and releases + +- **Current target: DuckDB v1.5.6.** The version is set in `DuckDB.NET.Bindings/Bindings.csproj` + (`DuckDbArtifactRoot`). The "Updated to DuckDB vX" line in the `PackageReleaseNotes` of both `Bindings.csproj` and + `Data.csproj` also needs updating. Every `LibraryImport` names its `EntryPoint`, so after an update, compare those + names with the library's exports to catch removed functions. +- **Package version:** comes from git tags through GitVersion (`GitVersion.yml`). DuckDB.NET versions follow the + DuckDB version (tag `1.5.6`). +- **Release steps:** + 1. Update the release notes in both project files. + 2. Merge `develop` into `main` and tag. + 3. Wait for the "Build" run on `main` to succeed. + 4. Start the `NuGet` workflow (`.github/workflows/NuGet.yml`, manual) on `main`. It publishes the + `nugetPackages-main` artifact of the latest *successful* main build, so starting it earlier publishes an old + build. Pushes to `develop` publish to GitHub Packages only. + +## Architecture + +### Bindings (`DuckDB.NET.Bindings`) + +- **`NativeMethods`:** a partial class split into one file per C API area under `NativeMethods/` (Appender, Arrow, + DataChunks, PreparedStatements, Query, ScalarFunction, StreamingResult, TableFunction, Value, Vectors, ...). Each + declaration is a `LibraryImport` with an explicit `EntryPoint` and the `DuckDbLibrary` constant; some hot paths + use `[SuppressGCTransition]`. +- **Lifetimes:** native objects are `SafeHandle`s in `DuckDBWrapperObjects.cs` (database, connection, prepared + statement, data chunk, logical type, value, ...). The exception is **`DuckDBResult`**, a plain struct in + `DuckDBNativeObjects.cs` with no finalizer. Every result must be `Close()`d exactly once, or it leaks permanently. +- **Strings:** returned strings use the marshallers in `DuckDBStringMarshallers.cs`. Use + `DuckDBOwnedStringMarshaller` when DuckDB owns the memory, and `DuckDBCallerOwnedStringMarshaller` when the caller + must `duckdb_free` it. + +### Connections (`DuckDB.NET.Data/Connection`) + +- **Pooling:** `DuckDBConnection` gets its native connection from the static `ConnectionManager`. It caches one + `FileReference` (native database plus a reference count) per file. Returning the last connection disposes the + database and removes the cache entry. +- **Data sources:** + - `:memory:` gives every connection its own private database; + - `:memory:?cache=shared` shares one; + - `md:?motherduck_token=...` connects to MotherDuck. +- **Configuration:** `DuckDBConnectionStringBuilder` normalizes `Data Source`/`DataSource` and exposes DuckDB + configuration options as typed properties. +- **Why not DuckDB's own instance cache:** `duckdb_get_or_create_from_cache` fails under concurrent opens of the same + path (duckdb/duckdb#22277), so `ConnectionManager` does the pooling itself. + +### Commands and results + +- **Execution:** `DuckDBCommand` calls `PreparedStatement.PrepareMultiple`, an iterator that extracts the statements + and prepares and executes one statement per `MoveNext()`. Parameters are converted by `DuckDBTypeMap` and + `ClrToDuckDBConverter` (positional and named). +- **Result modes:** `UseStreamingMode` **defaults to `false`**, so results are materialized. `true` uses + `duckdb_execute_prepared_streaming` and `duckdb_stream_fetch_chunk`. +- **Errors:** every native result error goes through `DuckDBResultExtensions.ThrowOnError`. An interrupt becomes + `OperationCanceledException`. An exception thrown by a C# UDF is attached as `InnerException` through + `UdfExceptionStore`, which is keyed by connection ID. Connection IDs repeat across databases, so parallel tests on + different databases can see each other's stored exception. This is a known flaky test source. +- **`DuckDBDataReader`:** reads one chunk at a time and creates readers through `VectorDataReaderFactory`, one per + column. Composite readers (struct, list, map, decimal) hold child readers. After the first chunk, readers are + reused through `VectorDataReaderBase.Reset(IntPtr vector)`. A new `VectorDataReaderBase` subclass must be + registered in the factory, and must override `Reset` if it has child readers. +- **Invariants in `DuckDBDataReader`, each guarded by a regression test:** + - A NULL chunk from a streaming fetch means either the end of the results or an error. Check the result's error + before treating it as the end. + - Reset the row counters *before* fetching, so a failed fetch cannot leave accessors reading the freed chunk. + - `NextResult()` closes the previous result: its chunk first, then the result. + - `Close()` releases the reader's own objects and statements *before* `CommandBehavior.CloseConnection` closes the + connection. Otherwise a still-alive prepared statement keeps the native database alive after `ConnectionManager` + disposes it, and the next open creates a second instance on the same file. That corrupts data on Linux and + macOS, and fails with "file in use" on Windows. +- **Arrow:** `ExecuteArrowStream` and `ExecuteArrowBatchesAsync` stream results as Apache Arrow record batches + (`Arrow/DuckDBArrowArrayStream.cs`), using DuckDB's Arrow C Data Interface. + +### Writing data: appenders and UDF output + +- **Writers:** `VectorDataWriterFactory` (`DataChunk/Writer`) creates the column writers used by the appender and by + scalar and table function output. `CollectionVectorDataWriter` holds the shared collection logic; + `ListVectorDataWriter` (running offsets, growth) and `ArrayVectorDataWriter` (fixed size, row-based positions) + are separate because DuckDB stores the two differently. +- **`DuckDBAppender`:** the low-level appender (`connection.CreateAppender(table)`, then `CreateRow().AppendValue(...)`, + or the allocation-free `AppendRow`). It buffers rows in a data chunk of `VectorSize` (2048) rows. It does **no type + checking** and writes the raw bytes of the .NET type it is given; the docs call this out. A row is written only once + every column has a value. Moving on from an incomplete row discards it and faults the appender. +- **`DuckDBMappedAppender`:** the type-checked alternative (`CreateAppender(table)`). A + `DuckDBAppenderMap` subclass declares the columns with `Map`, `DefaultValue` and `NullValue`, and type + mismatches are reported when the appender is created. See `AppenderMap-Usage.md`. +- **UDFs:** scalar functions (`DuckDBConnection.ScalarFunction*.cs`) and table functions + (`DuckDBConnection.TableFunction*.cs`) are registered through generic overloads for different parameter counts. + Callbacks are pinned with `GCHandle`. A table function creates its data enumerator per scan init, not per bind, + because DuckDB re-initializes scans without re-binding, for example in recursive CTEs. + +## Tests + +Test classes take the per-class `DuckDBDatabaseFixture` (a shared in-memory connection) through +`DuckDBTestBase`. Test classes run in parallel, each with its own database. `connection.ExecuteNonQuery(sql)` is +an extension in `Extensions/DbConnectionExtension.cs`. + +```csharp +public class MyTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db) +{ + [Fact] + public void TestSomething() + { + Connection.ExecuteNonQuery("CREATE TABLE ..."); + } +} +``` + +Tests that close or reconfigure a connection, or need a file database, should create their own `DuckDBConnection` +instead of using the shared fixture connection. + +Rules for every new or changed test (both have caused tests that passed locally and failed CI): +- **Match `AppendValue` argument types to the column types exactly.** A bare integer literal such as + `AppendValue(1)` binds to a 1-byte overload, and so does `AppendValue((int)1)`, because it is still a constant. + Written into an INTEGER column, that stores garbage. Use `(int?)1`, a typed variable, or the exact CLR type of + the column. +- **To check row order, use `ORDER BY rowid`** instead of adding an ordering column. +- **Run a new test at least 5 times before committing.** One pass proves nothing. + +**Known flaky failure:** with DuckDB 1.5.x, a later-chunk error test can fail with `OperationCanceledException`. +A worker-thread error sets the interrupt flag, and a streaming fetch can report that flag instead of the real error. +DuckDB fixes this in 2.0. Re-run the job; don't work around it. + +## Known debugging issue + +While debugging code that uses DuckDB.NET, `System.AccessViolationException` can appear because the debugger +interacts with native memory during marshalling. For the workaround, see +https://youtrack.jetbrains.com/issue/RIDER-114126. + +Building DuckDB extensions in C# is a separate project: https://github.com/Giorgi/DuckDB.ExtensionKit From 625d4cb5f856c54aaae70509b57a68c7c6a09efe Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 29 Sep 2026 11:38:29 +0400 Subject: [PATCH 05/26] Regenerate AGENTS.md Rewrite the agent guidance against the current code: build, test and native library commands, the DuckDB update and release process, the architecture of connections, commands, readers and writers, the reader invariants guarded by regression tests, coding style, and rules for writing tests. Fixes outdated claims, such as streaming being the default result mode. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- AGENTS.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 23b6b8bf..57fc1368 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ -# CLAUDE.md +# AGENTS.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to coding agents working with code in this repository. DuckDB.NET is an ADO.NET provider and low-level bindings for DuckDB. The solution (`DuckDB.NET.slnx`) has five projects: `Bindings` (P/Invoke over the DuckDB C API), `Data` (the ADO.NET provider), `Test` (xUnit), @@ -130,6 +130,21 @@ dotnet run -c Release --project DuckDB.NET.Benchmarks Callbacks are pinned with `GCHandle`. A table function creates its data enumerator per scan init, not per bind, because DuckDB re-initializes scans without re-binding, for example in recursive CTEs. +## Coding style + +There is no `.editorconfig`, so match the surrounding code: +- **Layout:** file-scoped namespaces (`namespace DuckDB.NET.Data;`) and 4-space indentation. +- **Naming:** `PascalCase` for types, methods and properties; `camelCase` for locals and fields, with no underscore + prefix. +- **Language features:** `LangVersion` is `latest`. Use `var` when the type is obvious. Primary constructors and + collection expressions (`[]`) are already used. +- **Nullable reference types:** enabled in `Bindings`, `Data` and `Benchmarks`, but **disabled in the test + project**. Don't put `?` on reference types in tests (`string?`, `List`); it produces CS8632 warnings. + `int?` and other nullable value types are fine. +- **Files:** files are UTF-8, and about half start with a byte-order mark. The checkout uses CRLF line endings + (`core.autocrlf`). Edits made with scripts must keep the file's byte-order mark as it is and keep CRLF throughout: + no stray LF, no doubled CR, and no lone CR at the end of a file. Otherwise git can treat the file as binary. + ## Tests Test classes take the per-class `DuckDBDatabaseFixture` (a shared in-memory connection) through From fcc6671274b229a60aaa0dd33f184e84b1ccbe34 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 02:13:24 +0400 Subject: [PATCH 06/26] Stop fetching after a streaming result ends. Fixes #358 DuckDB closes a streaming result when a fetch reaches its end, and every later fetch then fails with "Attempting to execute an unsuccessful or closed pending query result". Since f94d52b the reader throws the result's error after a NULL chunk, so a Read() after the end of a streaming result threw, and so did the first Read() of an empty result, whose end the constructor's fetch had already reached. The Arrow stream had the same problem. 1.5.5 returned false in both cases. DuckDBDataReader and DuckDBArrowArrayStream now remember a clean end of stream and stop fetching. An error while producing a chunk still throws, and the reader resets the flag when NextResult moves to a new result. New tests cover Read() after the end, an empty result, NextResult after reading past the end, and the Arrow stream, each in streaming and materialized mode. Fixes #358. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- .../Arrow/DuckDBArrowArrayStream.cs | 9 +++ DuckDB.NET.Data/DuckDBDataReader.cs | 11 ++++ DuckDB.NET.Test/Arrow/ArrowResultTests.cs | 23 ++++++++ DuckDB.NET.Test/DuckDBDataReaderTests.cs | 57 +++++++++++++++++++ 4 files changed, 100 insertions(+) diff --git a/DuckDB.NET.Data/Arrow/DuckDBArrowArrayStream.cs b/DuckDB.NET.Data/Arrow/DuckDBArrowArrayStream.cs index 39d71184..afbe72e1 100644 --- a/DuckDB.NET.Data/Arrow/DuckDBArrowArrayStream.cs +++ b/DuckDB.NET.Data/Arrow/DuckDBArrowArrayStream.cs @@ -19,6 +19,7 @@ internal sealed class DuckDBArrowArrayStream : IArrowArrayStream private readonly DuckDBArrowOptions arrowOptions; private readonly bool streaming; private bool disposed; + private bool ended; public Schema Schema { get; } @@ -112,6 +113,13 @@ private unsafe Schema BuildSchema() return new ValueTask(Task.FromCanceled(cancellationToken)); } + // DuckDB closes a streaming result when a fetch reaches its end, and every later fetch then fails with a + // "closed pending query result" error. Stop fetching after a clean end, so that error is not reported. + if (ended) + { + return new ValueTask((RecordBatch?)null); + } + var chunk = streaming ? NativeMethods.StreamingResult.DuckDBStreamFetchChunk(result) : NativeMethods.Query.DuckDBFetchChunk(result); @@ -125,6 +133,7 @@ private unsafe Schema BuildSchema() result.ThrowOnError(connection); } + ended = true; return new ValueTask((RecordBatch?)null); } diff --git a/DuckDB.NET.Data/DuckDBDataReader.cs b/DuckDB.NET.Data/DuckDBDataReader.cs index 02023e4e..06536ef9 100644 --- a/DuckDB.NET.Data/DuckDBDataReader.cs +++ b/DuckDB.NET.Data/DuckDBDataReader.cs @@ -21,6 +21,7 @@ public class DuckDBDataReader : DbDataReader private bool closed; private bool hasRows; private bool streamingResult; + private bool streamEnded; private long currentChunkIndex; private readonly IEnumerator resultEnumerator; @@ -67,6 +68,7 @@ private bool InitNextReader() currentResult.Close(); currentChunkIndex = 0; + streamEnded = false; currentResult = result; columnMapping = []; @@ -95,6 +97,13 @@ private bool InitNextReader() private bool InitChunkData() { + // DuckDB closes a streaming result when a fetch reaches its end, and every later fetch then fails with a + // "closed pending query result" error. Stop fetching after a clean end, so that error is not reported. + if (streamEnded) + { + return false; + } + var canReuse = vectorReaders.Length > 0; // Reset before fetching: if the fetch throws, the readers still point into the chunk freed here, @@ -107,7 +116,9 @@ private bool InitChunkData() if (streamingResult && currentChunk.IsInvalid) { + // No chunk and no error is the clean end of the stream; an error from producing the chunk still throws. currentResult.ThrowOnError(nativeConnection); + streamEnded = true; } currentChunkRowCount = NativeMethods.DataChunks.DuckDBDataChunkGetSize(currentChunk); diff --git a/DuckDB.NET.Test/Arrow/ArrowResultTests.cs b/DuckDB.NET.Test/Arrow/ArrowResultTests.cs index e797004f..5e40034b 100644 --- a/DuckDB.NET.Test/Arrow/ArrowResultTests.cs +++ b/DuckDB.NET.Test/Arrow/ArrowResultTests.cs @@ -130,6 +130,29 @@ public async Task ExecuteArrowStream_ReadsBatchesUntilNull() rows.Should().Be(10); } + // Regression test for https://github.com/Giorgi/DuckDB.NET/issues/358 + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ExecuteArrowStream_ReadAfterEndReturnsNull(bool useStreamingMode) + { + Command.UseStreamingMode = useStreamingMode; + Command.CommandText = "select i from range(5000) t(i)"; + + using var stream = Command.ExecuteArrowStream(); + + var rows = 0; + while (await stream.ReadNextRecordBatchAsync(CancellationToken.None) is { } batch) + { + rows += batch.Length; + batch.Dispose(); + } + + rows.Should().Be(5000); + (await stream.ReadNextRecordBatchAsync(CancellationToken.None)).Should().BeNull(); + (await stream.ReadNextRecordBatchAsync(CancellationToken.None)).Should().BeNull(); + } + [Fact] public void ExecuteArrowStream_ThrowsWhenNoResultSet() { diff --git a/DuckDB.NET.Test/DuckDBDataReaderTests.cs b/DuckDB.NET.Test/DuckDBDataReaderTests.cs index 370d32a9..80fbc140 100644 --- a/DuckDB.NET.Test/DuckDBDataReaderTests.cs +++ b/DuckDB.NET.Test/DuckDBDataReaderTests.cs @@ -454,6 +454,63 @@ public void ReadPivotStatementResult() reader.NextResult().Should().BeFalse(); } + // Regression tests for https://github.com/Giorgi/DuckDB.NET/issues/358 + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReadAfterEndOfResultReturnsFalse(bool useStreamingMode) + { + Command.UseStreamingMode = useStreamingMode; + Command.CommandText = "SELECT i FROM range(5000) t(i)"; + + using var reader = Command.ExecuteReader(); + + var rows = 0; + while (reader.Read()) + { + rows++; + } + + rows.Should().Be(5000); + reader.Read().Should().BeFalse(); + reader.Read().Should().BeFalse(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReadOnEmptyResultReturnsFalse(bool useStreamingMode) + { + Command.UseStreamingMode = useStreamingMode; + Command.CommandText = "SELECT i FROM range(0) t(i)"; + + using var reader = Command.ExecuteReader(); + + reader.HasRows.Should().BeFalse(); + reader.Read().Should().BeFalse(); + reader.Read().Should().BeFalse(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void NextResultAfterReadingPastEndReturnsNextResultSet(bool useStreamingMode) + { + Command.UseStreamingMode = useStreamingMode; + Command.CommandText = "SELECT i FROM range(0) t(i); SELECT 42"; + + using var reader = Command.ExecuteReader(); + + reader.Read().Should().BeFalse(); + reader.Read().Should().BeFalse(); + + reader.NextResult().Should().BeTrue(); + reader.Read().Should().BeTrue(); + reader.GetInt32(0).Should().Be(42); + reader.Read().Should().BeFalse(); + reader.Read().Should().BeFalse(); + } + [Fact] public void StreamingReadThrowsWhenLaterChunkFails() { From 549705c6a7ec282956940c05e9ea431796045c7c Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 02:18:42 +0400 Subject: [PATCH 07/26] Run the nightly build against DuckDB's v2.0 branch artifacts.duckdb.org// serves the nightly shared libraries of a DuckDB branch, and 'latest' is DuckDB's main, which is now 2.1 development. The fix for the wrong BIGNUM casts (duckdb/duckdb#25939) was merged into v2.0-cyanoptera only, so the nightly kept failing the same eight VarintTest cases against main. Download from the v2.0-cyanoptera path until DuckDB 2.0 ships. Linux, Windows and macOS archives are all published there. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- .github/workflows/ci.yml | 6 ++++-- AGENTS.md | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edce4ab3..18e285ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,12 +35,14 @@ jobs: echo "BuildBranch=nightly-builds" >> "$GITHUB_ENV" echo "BuildBranch=nightly-builds" >> $env:GITHUB_ENV + # Nightly native libraries: artifacts.duckdb.org//, where 'latest' is DuckDB's main branch. + # Tracks the v2.0 release branch until DuckDB 2.0 ships; switch back to 'latest' after that. - name: Set Env if: startsWith(github.ref_name, 'nightly') || startsWith(env.BuildBranch, 'nightly') run: | - echo "DuckDbArtifactRoot=https://artifacts.duckdb.org/latest" >> "$GITHUB_ENV" + echo "DuckDbArtifactRoot=https://artifacts.duckdb.org/v2.0-cyanoptera" >> "$GITHUB_ENV" echo "NightlyBuild=true" >> "$GITHUB_ENV" - echo "DuckDbArtifactRoot=https://artifacts.duckdb.org/latest" >> $env:GITHUB_ENV + echo "DuckDbArtifactRoot=https://artifacts.duckdb.org/v2.0-cyanoptera" >> $env:GITHUB_ENV echo "NightlyBuild=true" >> $env:GITHUB_ENV - name: Checkout diff --git a/AGENTS.md b/AGENTS.md index 57fc1368..c038c51f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,9 +36,10 @@ dotnet run -c Release --project DuckDB.NET.Benchmarks whatever `BuildType` is. - **The cache is version-blind:** a platform is downloaded only if its folder is missing. After changing the DuckDB version, delete `DuckDB.NET.Bindings/obj/runtimes`, rebuild, and confirm the version with `SELECT version()`. -- **Nightly builds:** `/p:NightlyBuild=true /p:DuckDbArtifactRoot=https://artifacts.duckdb.org/latest` downloads - DuckDB's `duckdb-shared-libs-.tar.gz` instead. CI does this on the schedule, against the - `nightly-builds` branch. +- **Nightly builds:** `/p:NightlyBuild=true /p:DuckDbArtifactRoot=https://artifacts.duckdb.org/` downloads + DuckDB's `duckdb-shared-libs-.tar.gz` instead. `` is a DuckDB branch name, and `latest` means + DuckDB's `main`. CI does this on the schedule: it uses the workflow file from `develop`, builds the code of the + `nightly-builds` branch, and currently tracks `v2.0-cyanoptera` until DuckDB 2.0 ships. ## DuckDB version and releases From 57142f286507c1fbedbde3ad6697740e243905f4 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 02:34:47 +0400 Subject: [PATCH 08/26] Append BigInteger to UHUGEINT columns as an unsigned value NumericVectorDataWriter.AppendBigInteger always wrote a DuckDBHugeInt, whatever the column type. On a UHUGEINT column a value of 2^127 or more threw ArgumentOutOfRangeException, and a negative value was written as its two's-complement bytes, so -1 was stored as a huge positive number. UHUGEINT[] list and array columns were affected too. Write a DuckDBUHugeInt when the column is UHUGEINT. Its constructor checks the range 0 to 2^128 - 1, so out-of-range values now throw. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- .../Writer/NumericVectorDataWriter.cs | 4 +- DuckDB.NET.Test/DuckDBManagedAppenderTests.cs | 65 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/DuckDB.NET.Data/DataChunk/Writer/NumericVectorDataWriter.cs b/DuckDB.NET.Data/DataChunk/Writer/NumericVectorDataWriter.cs index eb028dca..d90370fd 100644 --- a/DuckDB.NET.Data/DataChunk/Writer/NumericVectorDataWriter.cs +++ b/DuckDB.NET.Data/DataChunk/Writer/NumericVectorDataWriter.cs @@ -4,5 +4,7 @@ internal sealed unsafe class NumericVectorDataWriter(IntPtr vector, void* vector { internal override bool AppendNumeric(T value, ulong rowIndex) => AppendValueInternal(value, rowIndex); - internal override bool AppendBigInteger(BigInteger value, ulong rowIndex) => AppendValueInternal(new DuckDBHugeInt(value), rowIndex); + internal override bool AppendBigInteger(BigInteger value, ulong rowIndex) => ColumnType == DuckDBType.UnsignedHugeInt + ? AppendValueInternal(new DuckDBUHugeInt(value), rowIndex) + : AppendValueInternal(new DuckDBHugeInt(value), rowIndex); } diff --git a/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs b/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs index dc02d851..3d781079 100644 --- a/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs +++ b/DuckDB.NET.Test/DuckDBManagedAppenderTests.cs @@ -167,6 +167,71 @@ public void EmptyByteArrayIsEmptyBlobAndDefaultSpanIsNull() results.Should().Equal((false, 0L), (false, 0L), (true, null)); } + [Fact] + public void UnsignedHugeIntAboveSignedHugeIntRange() + { + // Only UHUGEINT columns, ordered by rowid: the low-level appender does no type checking. + Command.CommandText = "CREATE TABLE uhugeintAppenderTest(a UHUGEINT, b UHUGEINT[])"; + Command.ExecuteNonQuery(); + + BigInteger?[] values = + [ + BigInteger.Zero, + DuckDBHugeInt.HugeIntMaxValue, + BigInteger.Pow(2, 127) + 42, + DuckDBUHugeInt.HugeIntMaxValue, + ]; + + using (var appender = Connection.CreateAppender("uhugeintAppenderTest")) + { + foreach (var value in values) + { + appender.CreateRow().AppendValue(value).AppendValue(new List { value.Value, value.Value }).EndRow(); + } + } + + Command.CommandText = "SELECT a, b, a::VARCHAR FROM uhugeintAppenderTest ORDER BY rowid"; + using var reader = Command.ExecuteReader(); + + foreach (var value in values) + { + reader.Read().Should().BeTrue(); + reader.GetFieldValue(0).Should().Be(value.Value); + reader.GetFieldValue>(1).Should().Equal(value.Value, value.Value); + reader.GetString(2).Should().Be(value.Value.ToString()); + } + + reader.Read().Should().BeFalse(); + } + + [Fact] + public void UnsignedHugeIntRejectsValuesOutOfRange() + { + Command.CommandText = "CREATE TABLE uhugeintAppenderRangeTest(a UHUGEINT)"; + Command.ExecuteNonQuery(); + + BigInteger? negative = BigInteger.MinusOne; + BigInteger? tooLarge = DuckDBUHugeInt.HugeIntMaxValue + 1; + BigInteger? valid = BigInteger.Pow(2, 127); + + using (var appender = Connection.CreateAppender("uhugeintAppenderRangeTest")) + { + var row = appender.CreateRow(); + + row.Invoking(r => r.AppendValue(negative)).Should().Throw(); + row.Invoking(r => r.AppendValue(tooLarge)).Should().Throw(); + + row.AppendValue(valid).EndRow(); + } + + Command.CommandText = "SELECT a::VARCHAR FROM uhugeintAppenderRangeTest"; + using var reader = Command.ExecuteReader(); + + reader.Read().Should().BeTrue(); + reader.GetString(0).Should().Be(valid.Value.ToString()); + reader.Read().Should().BeFalse(); + } + [Fact] public void Decimals() { From 3162cae77e5334fcbba2fa076266096468e6b7ec Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 02:53:58 +0400 Subject: [PATCH 09/26] Make the late streaming error tests work on DuckDB 2.0 Four tests need a streaming query to fail only after some rows were read. With default settings that no longer happens on DuckDB 2.0: it buffers past the failing row, discards the buffered rows when the error occurs, and reports the error from ExecuteReader, before any row is returned. On DuckDB 1.5 the same tests sometimes failed with OperationCanceledException, because an error on a worker thread can be reported as an interrupt. The tests now use a private in-memory connection with threads=1 and a 100KB streaming buffer. One thread produces the chunks in order and can get only a few chunks ahead of the reader, so the rows before the failing one are read first on both versions. One thread alone was not enough on 2.0. streaming_buffer_size is a per-connection setting, so it is applied with SET; the connection string rejects it. Verified on DuckDB 1.5.6 and on v2.0.0-alpha44255: the four tests pass 6 runs out of 6 on each. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Test/Arrow/ArrowResultTests.cs | 8 +++++--- DuckDB.NET.Test/DuckDBDataReaderTests.cs | 11 ++++++----- DuckDB.NET.Test/DuckDBDatabaseFixture.cs | 20 ++++++++++++++++++++ DuckDB.NET.Test/ScalarFunctionTests.cs | 16 +++++++++------- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/DuckDB.NET.Test/Arrow/ArrowResultTests.cs b/DuckDB.NET.Test/Arrow/ArrowResultTests.cs index 5e40034b..67f32458 100644 --- a/DuckDB.NET.Test/Arrow/ArrowResultTests.cs +++ b/DuckDB.NET.Test/Arrow/ArrowResultTests.cs @@ -82,10 +82,12 @@ public async Task ExecuteArrowBatches_StreamsMultipleChunks(bool useStreamingMod public async Task ExecuteArrowStream_ThrowsWhenLaterStreamingChunkFails() { // The first chunks convert fine; the error is raised only while producing a later chunk. - Command.UseStreamingMode = true; - Command.CommandText = "SELECT CAST(CASE WHEN i < 500000 THEN CAST(i AS VARCHAR) ELSE 'not a number' END AS INTEGER) FROM range(1000000) t(i)"; + using var connection = OpenConnectionForLateStreamingErrors(); + using var command = connection.CreateCommand(); + command.UseStreamingMode = true; + command.CommandText = "SELECT CAST(CASE WHEN i < 500000 THEN CAST(i AS VARCHAR) ELSE 'not a number' END AS INTEGER) FROM range(1000000) t(i)"; - using var stream = Command.ExecuteArrowStream(); + using var stream = command.ExecuteArrowStream(); var rows = 0; var act = async () => diff --git a/DuckDB.NET.Test/DuckDBDataReaderTests.cs b/DuckDB.NET.Test/DuckDBDataReaderTests.cs index 80fbc140..797feba4 100644 --- a/DuckDB.NET.Test/DuckDBDataReaderTests.cs +++ b/DuckDB.NET.Test/DuckDBDataReaderTests.cs @@ -182,8 +182,7 @@ public void FailedExecuteReaderClosesConnectionOnlyWithCloseConnection(string sq public void StreamingReadFailureClosesConnectionOnlyWhenReaderIsDisposed() { // Matches Npgsql: a failed Read() leaves the connection open, and disposing the reader closes it. - using var connection = new DuckDBConnection("DataSource=:memory:"); - connection.Open(); + using var connection = OpenConnectionForLateStreamingErrors(); using var command = connection.CreateCommand(); command.UseStreamingMode = true; command.CommandText = "SELECT CAST(CASE WHEN i < 500000 THEN CAST(i AS VARCHAR) ELSE 'not a number' END AS INTEGER) FROM range(1000000) t(i)"; @@ -515,10 +514,12 @@ public void NextResultAfterReadingPastEndReturnsNextResultSet(bool useStreamingM public void StreamingReadThrowsWhenLaterChunkFails() { // The first chunks convert fine; the error is raised only while producing a later chunk. - Command.UseStreamingMode = true; - Command.CommandText = "SELECT CAST(CASE WHEN i < 500000 THEN CAST(i AS VARCHAR) ELSE 'not a number' END AS INTEGER) FROM range(1000000) t(i)"; + using var connection = OpenConnectionForLateStreamingErrors(); + using var command = connection.CreateCommand(); + command.UseStreamingMode = true; + command.CommandText = "SELECT CAST(CASE WHEN i < 500000 THEN CAST(i AS VARCHAR) ELSE 'not a number' END AS INTEGER) FROM range(1000000) t(i)"; - using var reader = Command.ExecuteReader(); + using var reader = command.ExecuteReader(); var rows = 0; var act = () => diff --git a/DuckDB.NET.Test/DuckDBDatabaseFixture.cs b/DuckDB.NET.Test/DuckDBDatabaseFixture.cs index c9cf7dbb..c5e33f75 100644 --- a/DuckDB.NET.Test/DuckDBDatabaseFixture.cs +++ b/DuckDB.NET.Test/DuckDBDatabaseFixture.cs @@ -44,4 +44,24 @@ public virtual void Dispose() { Command?.Dispose(); } + + /// + /// Opens a private in-memory database for tests where a streaming query must fail only after some rows were read. + /// One thread produces the chunks in order, and a small streaming buffer lets it get only a few chunks ahead of + /// the reader, so the rows before the failing one are read first. With the default settings DuckDB 2.0 buffers + /// past the failing row and reports the error on the first fetch, and DuckDB 1.5 can report such an error as an + /// interrupt when several threads are running. + /// + protected static DuckDBConnection OpenConnectionForLateStreamingErrors() + { + var connection = new DuckDBConnection("DataSource=:memory:;threads=1"); + connection.Open(); + + // A per-connection setting, so it cannot go into the connection string. + using var command = connection.CreateCommand(); + command.CommandText = "SET streaming_buffer_size = '100KB'"; + command.ExecuteNonQuery(); + + return connection; + } } \ No newline at end of file diff --git a/DuckDB.NET.Test/ScalarFunctionTests.cs b/DuckDB.NET.Test/ScalarFunctionTests.cs index 336cf8e2..63c64b0b 100644 --- a/DuckDB.NET.Test/ScalarFunctionTests.cs +++ b/DuckDB.NET.Test/ScalarFunctionTests.cs @@ -113,13 +113,15 @@ public void RegisterScalarFunctionCallbackThrows() [Fact] public void RegisterScalarFunctionCallbackThrowsInLaterStreamingChunk() { - Connection.RegisterScalarFunction("throwing_late_scalar", + using var connection = OpenConnectionForLateStreamingErrors(); + connection.RegisterScalarFunction("throwing_late_scalar", x => x < 500_000 ? x : throw new InvalidOperationException("Scalar callback failed late")); - Command.UseStreamingMode = true; - Command.CommandText = "SELECT throwing_late_scalar(i) FROM range(1000000) t(i)"; + using var command = connection.CreateCommand(); + command.UseStreamingMode = true; + command.CommandText = "SELECT throwing_late_scalar(i) FROM range(1000000) t(i)"; - using (var reader = Command.ExecuteReader()) + using (var reader = command.ExecuteReader()) { var rows = 0; var act = () => @@ -137,10 +139,10 @@ public void RegisterScalarFunctionCallbackThrowsInLaterStreamingChunk() } // The stored callback exception was consumed, so it must not leak into the next error on this connection. - Command.UseStreamingMode = false; - Command.CommandText = "SELECT CAST('not a number' AS INTEGER)"; + command.UseStreamingMode = false; + command.CommandText = "SELECT CAST('not a number' AS INTEGER)"; - Command.Invoking(command => command.ExecuteScalar()) + command.Invoking(c => c.ExecuteScalar()) .Should().Throw() .Where(e => e.InnerException == null); } From e817f6a19da3139338054279284bc03c7d6fa4d8 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 03:16:05 +0400 Subject: [PATCH 10/26] Test the mapped appender with every supported property type The mapped appender tests used only int, string, float, DateTime and byte[] properties, so most branches of PropertyMapping.AppendToRow never ran. A new test maps bool, sbyte, short, long, byte, ushort, uint, ulong, double, decimal, DateTimeOffset, TimeSpan, Guid, BigInteger, DateOnly and TimeOnly, plus a nullable int and a null string, and reads the values back. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Test/DuckDBMappedAppenderTests.cs | 135 +++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs b/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs index 9186bd58..2527954c 100644 --- a/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs +++ b/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs @@ -185,4 +185,139 @@ public void MappedAppender_SupportsDefaultAndNull() reader.GetInt32(2).Should().Be(18); reader.IsDBNull(3).Should().BeTrue(); } + + // One property for every type the mapped appender supports that the other tests do not use. + public class AllTypes + { + public bool Bool { get; set; } + public sbyte SByte { get; set; } + public short Short { get; set; } + public long Long { get; set; } + public byte Byte { get; set; } + public ushort UShort { get; set; } + public uint UInt { get; set; } + public ulong ULong { get; set; } + public double Double { get; set; } + public decimal Decimal { get; set; } + public DateTimeOffset DateTimeOffset { get; set; } + public TimeSpan TimeSpan { get; set; } + public Guid Guid { get; set; } + public BigInteger BigInteger { get; set; } + public DateOnly DateOnly { get; set; } + public TimeOnly TimeOnly { get; set; } + public int? NullableInt { get; set; } + public string NullableString { get; set; } + } + + public class AllTypesMap : DuckDBAppenderMap + { + public AllTypesMap() + { + Map(x => x.Bool); + Map(x => x.SByte); + Map(x => x.Short); + Map(x => x.Long); + Map(x => x.Byte); + Map(x => x.UShort); + Map(x => x.UInt); + Map(x => x.ULong); + Map(x => x.Double); + Map(x => x.Decimal); + Map(x => x.DateTimeOffset); + Map(x => x.TimeSpan); + Map(x => x.Guid); + Map(x => x.BigInteger); + Map(x => x.DateOnly); + Map(x => x.TimeOnly); + Map(x => x.NullableInt); + Map(x => x.NullableString); + } + } + + [Fact] + public void MappedAppender_SupportsAllMappedTypes() + { + Command.CommandText = """ + CREATE TABLE mapped_all_types( + c_bool BOOLEAN, c_tinyint TINYINT, c_smallint SMALLINT, c_bigint BIGINT, + c_utinyint UTINYINT, c_usmallint USMALLINT, c_uinteger UINTEGER, c_ubigint UBIGINT, + c_double DOUBLE, c_decimal DECIMAL(18, 4), c_timestamptz TIMESTAMPTZ, c_interval INTERVAL, + c_uuid UUID, c_hugeint HUGEINT, c_date DATE, c_time TIME, + c_nullable_int INTEGER, c_nullable_string VARCHAR); + """; + Command.ExecuteNonQuery(); + + var record = new AllTypes + { + Bool = true, + SByte = -8, + Short = -1600, + Long = -6_400_000_000_000, + Byte = 200, + UShort = 60_000, + UInt = 4_000_000_000, + ULong = 18_000_000_000_000_000_000, + Double = 1234.5678, + Decimal = 12345.6789m, + DateTimeOffset = new DateTimeOffset(2024, 5, 17, 13, 45, 30, 123, TimeSpan.Zero), + TimeSpan = new TimeSpan(3, 4, 5, 6, 789), + Guid = Guid.Parse("0f8fad5b-d9cb-469f-a165-70867728950e"), + BigInteger = BigInteger.Parse("123456789012345678901234567890"), + DateOnly = new DateOnly(2024, 5, 17), + TimeOnly = new TimeOnly(13, 45, 30, 123), + NullableInt = 42, + NullableString = "text", + }; + + // The same values again, but with the two nullable properties left null. + var recordWithNulls = new AllTypes + { + Bool = record.Bool, SByte = record.SByte, Short = record.Short, Long = record.Long, + Byte = record.Byte, UShort = record.UShort, UInt = record.UInt, ULong = record.ULong, + Double = record.Double, Decimal = record.Decimal, DateTimeOffset = record.DateTimeOffset, + TimeSpan = record.TimeSpan, Guid = record.Guid, BigInteger = record.BigInteger, + DateOnly = record.DateOnly, TimeOnly = record.TimeOnly, + }; + + using (var appender = Connection.CreateAppender("mapped_all_types")) + { + appender.AppendRecords(new[] { record, recordWithNulls }); + } + + Command.CommandText = "SELECT * FROM mapped_all_types ORDER BY rowid"; + using var reader = Command.ExecuteReader(); + + for (var row = 0; row < 2; row++) + { + reader.Read().Should().BeTrue(); + + reader.GetBoolean(0).Should().Be(record.Bool); + reader.GetFieldValue(1).Should().Be(record.SByte); + reader.GetInt16(2).Should().Be(record.Short); + reader.GetInt64(3).Should().Be(record.Long); + reader.GetByte(4).Should().Be(record.Byte); + reader.GetFieldValue(5).Should().Be(record.UShort); + reader.GetFieldValue(6).Should().Be(record.UInt); + reader.GetFieldValue(7).Should().Be(record.ULong); + reader.GetDouble(8).Should().Be(record.Double); + reader.GetDecimal(9).Should().Be(record.Decimal); + reader.GetFieldValue(10).UtcDateTime.Should().Be(record.DateTimeOffset.UtcDateTime); + reader.GetFieldValue(11).Should().Be(record.TimeSpan); + reader.GetGuid(12).Should().Be(record.Guid); + reader.GetFieldValue(13).Should().Be(record.BigInteger); + reader.GetFieldValue(14).Should().Be(record.DateOnly); + reader.GetFieldValue(15).Should().Be(record.TimeOnly); + } + + Command.CommandText = "SELECT c_nullable_int, c_nullable_string FROM mapped_all_types ORDER BY rowid"; + using var nullableReader = Command.ExecuteReader(); + + nullableReader.Read().Should().BeTrue(); + nullableReader.GetInt32(0).Should().Be(42); + nullableReader.GetString(1).Should().Be("text"); + + nullableReader.Read().Should().BeTrue(); + nullableReader.IsDBNull(0).Should().BeTrue(); + nullableReader.IsDBNull(1).Should().BeTrue(); + } } From 2b513daa700781c7b77bd638dd84b078c93f9278 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 03:21:12 +0400 Subject: [PATCH 11/26] Update coverlet to 10.1.0 Coverlet 6.0.0 counted the copy constructor the compiler generates for a record without a primary constructor as uncovered code, so the record line of ScalarFunctionOptions showed as missed (coverlet-coverage/coverlet#1633). 10.1.0 no longer reports it. The coverage report is still written to DuckDB.NET.Test/coverage.net8.0.info. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Test/Test.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDB.NET.Test/Test.csproj b/DuckDB.NET.Test/Test.csproj index 37ced068..0e01f370 100644 --- a/DuckDB.NET.Test/Test.csproj +++ b/DuckDB.NET.Test/Test.csproj @@ -11,10 +11,10 @@ - + - + From 86b62fae53ab0aaec2f156605b44075c82689991 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 03:24:30 +0400 Subject: [PATCH 12/26] Test the configuration option descriptor's conversions The descriptor for a DuckDB configuration option reads its value from the connection string as text and converts it to the option's type, and writes values back as invariant text. Only the UBIGINT read and write and one boolean write were tested. New tests cover reading BOOLEAN, BIGINT, UBIGINT, DOUBLE and text options, text that does not fit the type, empty text, writing under a culture with a decimal comma, writing null, and the descriptor's metadata (ComponentType, IsReadOnly, CanResetValue). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Test/ConnectionStringTests.cs | 90 ++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/DuckDB.NET.Test/ConnectionStringTests.cs b/DuckDB.NET.Test/ConnectionStringTests.cs index 702b6ef2..afb50b85 100644 --- a/DuckDB.NET.Test/ConnectionStringTests.cs +++ b/DuckDB.NET.Test/ConnectionStringTests.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Globalization; namespace DuckDB.NET.Test; @@ -542,6 +543,95 @@ public void BooleanOptionDescriptorsWriteValuesDuckDBAccepts() connection.State.Should().Be(ConnectionState.Open); } + [Fact] + public void ConfigurationOptionDescriptorsDescribeTheBuilder() + { + var builder = new DuckDBConnectionStringBuilder { DataSource = DuckDBConnectionStringBuilder.InMemoryDataSource }; + var option = TypeDescriptor.GetProperties(builder).Find("zstd_min_string_length", false)!; + + option.ComponentType.Should().Be(typeof(DuckDBConnectionStringBuilder)); + option.IsReadOnly.Should().BeFalse(); + + option.CanResetValue(builder).Should().BeFalse(); + option.SetValue(builder, 8192UL); + option.CanResetValue(builder).Should().BeTrue(); + + // A component that is not a connection string builder has no value to read. + option.GetValue(new object()).Should().BeNull(); + } + + // The connection string stores every value as text, so reading converts it back to the option's type. Text that + // does not fit the type reads as null instead of throwing out of a property grid. + [Theory] + [InlineData("arrow_large_buffer_size", "yes", true)] + [InlineData("arrow_large_buffer_size", "0", false)] + [InlineData("arrow_large_buffer_size", "maybe", null)] + [InlineData("geometry_minimum_shredding_size", "-123", -123L)] + [InlineData("geometry_minimum_shredding_size", "abc", null)] + [InlineData("zstd_min_string_length", "8192", 8192UL)] + [InlineData("zstd_min_string_length", "-1", null)] + [InlineData("index_scan_percentage", "0.001", 0.001)] + [InlineData("index_scan_percentage", "abc", null)] + [InlineData("default_secret_storage", "local_file", "local_file")] + public void ConfigurationOptionDescriptorsConvertTextToTheOptionType(string optionName, string text, object expected) + { + var builder = new DuckDBConnectionStringBuilder { DataSource = DuckDBConnectionStringBuilder.InMemoryDataSource }; + var option = TypeDescriptor.GetProperties(builder).Find(optionName, false)!; + + builder[optionName] = text; + + option.GetValue(builder).Should().Be(expected); + } + + [Theory] + [InlineData("default_secret_storage", "")] + [InlineData("zstd_min_string_length", null)] + public void ConfigurationOptionDescriptorsReadEmptyTextAsEmptyOrNull(string optionName, string expected) + { + var builder = new DuckDBConnectionStringBuilder { DataSource = DuckDBConnectionStringBuilder.InMemoryDataSource }; + var option = TypeDescriptor.GetProperties(builder).Find(optionName, false)!; + + builder[optionName] = ""; + + // Only a text option can hold an empty value; for any other type it means "not set". + option.GetValue(builder).Should().Be(expected); + } + + [Fact] + public void ConfigurationOptionDescriptorsWriteInvariantText() + { + var builder = new DuckDBConnectionStringBuilder { DataSource = DuckDBConnectionStringBuilder.InMemoryDataSource }; + var properties = TypeDescriptor.GetProperties(builder); + var previousCulture = CultureInfo.CurrentCulture; + + try + { + // German formats 0.001 as "0,001", which DuckDB would reject. + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de-DE"); + + properties.Find("index_scan_percentage", false)!.SetValue(builder, 0.001); + properties.Find("arrow_large_buffer_size", false)!.SetValue(builder, false); + properties.Find("default_secret_storage", false)!.SetValue(builder, "local_file"); + + // Anything else is written with its ToString(). + properties.Find("zstd_min_string_length", false)!.SetValue(builder, new StringBuilder("8192")); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + + builder["index_scan_percentage"].Should().Be("0.001"); + builder["arrow_large_buffer_size"].Should().Be("false"); + builder["default_secret_storage"].Should().Be("local_file"); + builder["zstd_min_string_length"].Should().Be("8192"); + + // Writing null removes the option. + properties.Find("default_secret_storage", false)!.SetValue(builder, null); + + builder.ContainsKey("default_secret_storage").Should().BeFalse(); + } + [Fact] public void OptionsThatCannotBeAppliedAtOpenAreNotOffered() { From 00cbc8c80aaba8ef55f9ab2ce10acf567ccc84eb Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 03:28:05 +0400 Subject: [PATCH 13/26] Test every typed property of the connection string builder Many typed configuration properties of DuckDBConnectionStringBuilder had a getter or setter that no test used. A reflection-driven test now sets each typed property to sample values, reads them back, and clears them, so a property added later is covered too. Further tests cover the spellings DefaultOrder accepts, malformed sort order values, undefined enum values, and empty number and boolean options. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Test/ConnectionStringTests.cs | 98 ++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/DuckDB.NET.Test/ConnectionStringTests.cs b/DuckDB.NET.Test/ConnectionStringTests.cs index afb50b85..488bb6fd 100644 --- a/DuckDB.NET.Test/ConnectionStringTests.cs +++ b/DuckDB.NET.Test/ConnectionStringTests.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Globalization; +using System.Reflection; namespace DuckDB.NET.Test; @@ -350,6 +351,103 @@ public void TypedPropertiesThrowOnMalformedValues() builder.Invoking(b => b.AccessMode).Should().Throw(); } + // Every typed property, found by reflection, so a property added later is covered without a change here. + public static IEnumerable TypedPropertyNames() => + typeof(DuckDBConnectionStringBuilder) + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Where(property => property.GetCustomAttribute() != null) + .Select(property => new object[] { property.Name }); + + [Theory] + [MemberData(nameof(TypedPropertyNames))] + public void EveryTypedPropertyRoundTripsAndClears(string propertyName) + { + var property = typeof(DuckDBConnectionStringBuilder).GetProperty(propertyName)!; + var keyword = property.GetCustomAttribute()!.DisplayName; + var type = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + + object[] samples; + + if (type == typeof(string)) + { + samples = ["sample"]; + } + else if (type == typeof(bool)) + { + samples = [true, false]; + } + else if (type == typeof(int)) + { + samples = [3]; + } + else if (type.IsEnum) + { + samples = Enum.GetValues(type).Cast().ToArray(); + } + else + { + throw new NotSupportedException($"Add sample values for {type.Name}, the type of {propertyName}."); + } + + foreach (var sample in samples) + { + var builder = new DuckDBConnectionStringBuilder(); + + property.SetValue(builder, sample); + + builder.ContainsKey(keyword).Should().BeTrue($"setting {propertyName} should write '{keyword}'"); + property.GetValue(builder).Should().Be(sample); + + property.SetValue(builder, null); + + builder.ContainsKey(keyword).Should().BeFalse($"setting {propertyName} to null should remove '{keyword}'"); + property.GetValue(builder).Should().Be(type == typeof(string) ? "" : null); + } + } + + [Theory] + [InlineData("asc", DuckDBSortOrder.Ascending)] + [InlineData("ASCENDING", DuckDBSortOrder.Ascending)] + [InlineData("desc", DuckDBSortOrder.Descending)] + [InlineData("Descending", DuckDBSortOrder.Descending)] + public void DefaultOrderAcceptsEverySpellingDuckDBAccepts(string value, DuckDBSortOrder expected) + { + var builder = new DuckDBConnectionStringBuilder { ConnectionString = $"DataSource=:memory:;default_order={value}" }; + + builder.DefaultOrder.Should().Be(expected); + } + + [Fact] + public void SortOrderPropertiesThrowOnMalformedValues() + { + var builder = new DuckDBConnectionStringBuilder { ConnectionString = "DataSource=:memory:;default_order=sideways;default_null_order=middle" }; + + builder.Invoking(b => b.DefaultOrder).Should().Throw(); + builder.Invoking(b => b.DefaultNullOrder).Should().Throw(); + } + + [Fact] + public void EnumPropertiesRejectUndefinedValues() + { + var builder = new DuckDBConnectionStringBuilder(); + + builder.Invoking(b => b.AccessMode = (DuckDBAccessMode)999).Should().Throw(); + builder.Invoking(b => b.DefaultOrder = (DuckDBSortOrder)999).Should().Throw(); + builder.Invoking(b => b.DefaultNullOrder = (DuckDBNullOrder)999).Should().Throw(); + } + + [Fact] + public void EmptyNumberAndBooleanOptionsReadAsNotSet() + { + var builder = new DuckDBConnectionStringBuilder { DataSource = DuckDBConnectionStringBuilder.InMemoryDataSource }; + + builder["threads"] = ""; + builder["enable_external_access"] = ""; + + builder.Threads.Should().BeNull(); + builder.EnableExternalAccess.Should().BeNull(); + } + [Fact] public void TypedPropertiesProduceAConnectionStringDuckDBAccepts() { From f3f401fd435fc852ea59268840c0df1aaede53bb Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 12:10:27 +0400 Subject: [PATCH 14/26] Support DuckDBDateOnly and DuckDBTimeOnly in the mapped appender and UDFs The mapped appender rejected properties of type DuckDBDateOnly and DuckDBTimeOnly with "not supported for mapping", although the low-level appender and the mapping code handle both. DuckDBDateOnly matters because it can hold the infinite dates that DateOnly cannot. Both types are now in the CLR-to-DuckDB type map, as DATE and TIME. The map also decides the types of user-defined functions, so scalar functions and table function columns and parameters accept them too. Table function parameters needed a fix: DuckDBValue.GetValue always produced a DateOnly or TimeOnly and reinterpreted it as T without a check, so GetValue() returned garbage. It now produces the requested type for DATE and TIME values. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Bindings/DuckDBWrapperObjects.cs | 4 ++ DuckDB.NET.Data/Extensions/TypeExtensions.cs | 4 +- DuckDB.NET.Test/DuckDBMappedAppenderTests.cs | 66 ++++++++++++++++++++ DuckDB.NET.Test/ScalarFunctionTests.cs | 21 +++++++ DuckDB.NET.Test/TableFunctionTests.cs | 35 +++++++++++ 5 files changed, 129 insertions(+), 1 deletion(-) diff --git a/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs b/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs index 343074cf..50736017 100644 --- a/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs +++ b/DuckDB.NET.Bindings/DuckDBWrapperObjects.cs @@ -167,7 +167,11 @@ public T GetValue() DuckDBType.Varchar => Cast(NativeMethods.Value.DuckDBGetVarchar(this)), + // DATE and TIME can be read as the .NET types or as the provider's own types. Cast reinterprets the value + // without a check, so the value has to be produced as exactly the requested type. + DuckDBType.Date when typeof(T) == typeof(DuckDBDateOnly) => Cast(DuckDBDateOnly.FromDuckDBDate(NativeMethods.Value.DuckDBGetDate(this))), DuckDBType.Date => Cast((DateOnly)DuckDBDateOnly.FromDuckDBDate(NativeMethods.Value.DuckDBGetDate(this))), + DuckDBType.Time when typeof(T) == typeof(DuckDBTimeOnly) => Cast(NativeMethods.DateTimeHelpers.DuckDBFromTime(NativeMethods.Value.DuckDBGetTime(this))), DuckDBType.Time => Cast((TimeOnly)NativeMethods.DateTimeHelpers.DuckDBFromTime(NativeMethods.Value.DuckDBGetTime(this))), DuckDBType.TimeTz => Cast(GetTimeTzValue()), DuckDBType.Interval => Cast((TimeSpan)NativeMethods.Value.DuckDBGetInterval(this)), diff --git a/DuckDB.NET.Data/Extensions/TypeExtensions.cs b/DuckDB.NET.Data/Extensions/TypeExtensions.cs index a95c6a46..2ff375c5 100644 --- a/DuckDB.NET.Data/Extensions/TypeExtensions.cs +++ b/DuckDB.NET.Data/Extensions/TypeExtensions.cs @@ -34,6 +34,8 @@ internal static class TypeExtensions { typeof(TimeSpan), DuckDBType.Interval}, { typeof(DateOnly), DuckDBType.Date}, { typeof(TimeOnly), DuckDBType.Time}, + { typeof(DuckDBDateOnly), DuckDBType.Date}, + { typeof(DuckDBTimeOnly), DuckDBType.Time}, { typeof(DateTimeOffset), DuckDBType.TimestampTz}, { typeof(BigInteger), DuckDBType.HugeInt}, { typeof(string), DuckDBType.Varchar}, @@ -105,5 +107,5 @@ public static DuckDBLogicalType GetLogicalType(this Type type) throw new InvalidOperationException($"Cannot map type {type.FullName} to DuckDBType."); } - public static DuckDBType GetDuckDBType(this Type type) => ClrToDuckDBTypeMap.TryGetValue(type, out var duckDBType) ? duckDBType : DuckDBType.Invalid; + public static DuckDBType GetDuckDBType(this Type type) => ClrToDuckDBTypeMap.GetValueOrDefault(type, DuckDBType.Invalid); } \ No newline at end of file diff --git a/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs b/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs index 2527954c..0815a27d 100644 --- a/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs +++ b/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs @@ -320,4 +320,70 @@ CREATE TABLE mapped_all_types( nullableReader.IsDBNull(0).Should().BeTrue(); nullableReader.IsDBNull(1).Should().BeTrue(); } + // The provider's own date and time types. DuckDBDateOnly can hold the infinite dates that DateOnly cannot. + public class DuckDBDateAndTime + { + public DuckDBDateOnly Date { get; set; } + public DuckDBTimeOnly Time { get; set; } + public DuckDBDateOnly? NullableDate { get; set; } + public DuckDBTimeOnly? NullableTime { get; set; } + } + + public class DuckDBDateAndTimeMap : DuckDBAppenderMap + { + public DuckDBDateAndTimeMap() + { + Map(x => x.Date); + Map(x => x.Time); + Map(x => x.NullableDate); + Map(x => x.NullableTime); + } + } + + [Fact] + public void MappedAppender_SupportsDuckDBDateOnlyAndTimeOnly() + { + Command.CommandText = "CREATE TABLE mapped_duckdb_date_time(d DATE, t TIME, nullable_d DATE, nullable_t TIME);"; + Command.ExecuteNonQuery(); + + var records = new[] + { + new DuckDBDateAndTime + { + Date = new DuckDBDateOnly(2024, 5, 17), + Time = new DuckDBTimeOnly(13, 45, 30, 123456), + NullableDate = new DuckDBDateOnly(2000, 1, 2), + NullableTime = new DuckDBTimeOnly(1, 2, 3), + }, + new DuckDBDateAndTime { Date = DuckDBDateOnly.PositiveInfinity, Time = new DuckDBTimeOnly(0, 0, 0) }, + new DuckDBDateAndTime { Date = DuckDBDateOnly.NegativeInfinity, Time = new DuckDBTimeOnly(23, 59, 59, 999999) }, + }; + + using (var appender = Connection.CreateAppender("mapped_duckdb_date_time")) + { + appender.AppendRecords(records); + } + + Command.CommandText = "SELECT d, t, nullable_d, nullable_t, d::VARCHAR FROM mapped_duckdb_date_time ORDER BY rowid"; + using var reader = Command.ExecuteReader(); + + reader.Read().Should().BeTrue(); + reader.GetFieldValue(0).Should().Be(records[0].Date); + reader.GetFieldValue(1).Should().Be(records[0].Time); + reader.GetFieldValue(2).Should().Be(records[0].NullableDate.Value); + reader.GetFieldValue(3).Should().Be(records[0].NullableTime.Value); + reader.GetString(4).Should().Be("2024-05-17"); + + reader.Read().Should().BeTrue(); + reader.GetFieldValue(0).IsPositiveInfinity.Should().BeTrue(); + reader.GetFieldValue(1).Should().Be(records[1].Time); + reader.IsDBNull(2).Should().BeTrue(); + reader.IsDBNull(3).Should().BeTrue(); + reader.GetString(4).Should().Be("infinity"); + + reader.Read().Should().BeTrue(); + reader.GetFieldValue(0).IsNegativeInfinity.Should().BeTrue(); + reader.GetFieldValue(1).Should().Be(records[2].Time); + reader.GetString(4).Should().Be("-infinity"); + } } diff --git a/DuckDB.NET.Test/ScalarFunctionTests.cs b/DuckDB.NET.Test/ScalarFunctionTests.cs index 63c64b0b..f4bf788b 100644 --- a/DuckDB.NET.Test/ScalarFunctionTests.cs +++ b/DuckDB.NET.Test/ScalarFunctionTests.cs @@ -147,6 +147,27 @@ public void RegisterScalarFunctionCallbackThrowsInLaterStreamingChunk() .Where(e => e.InnerException == null); } + [Fact] + public void RegisterScalarFunctionWithDuckDBDateOnlyAndTimeOnly() + { + Connection.RegisterScalarFunction("same_duckdb_date", date => date); + Connection.RegisterScalarFunction("same_duckdb_time", time => time); + + Command.CommandText = """ + SELECT same_duckdb_date(DATE '2024-05-17')::VARCHAR, same_duckdb_date('infinity'::DATE)::VARCHAR, + same_duckdb_time(TIME '13:45:30.123456')::VARCHAR, + typeof(same_duckdb_date(DATE '2024-05-17')), typeof(same_duckdb_time(TIME '13:45:30')) + """; + using var reader = Command.ExecuteReader(); + + reader.Read().Should().BeTrue(); + reader.GetString(0).Should().Be("2024-05-17"); + reader.GetString(1).Should().Be("infinity"); + reader.GetString(2).Should().Be("13:45:30.123456"); + reader.GetString(3).Should().Be("DATE"); + reader.GetString(4).Should().Be("TIME"); + } + [Fact] public void RegisterScalarFunctionIsPrime() { diff --git a/DuckDB.NET.Test/TableFunctionTests.cs b/DuckDB.NET.Test/TableFunctionTests.cs index 0120e8e4..91c5ccf7 100644 --- a/DuckDB.NET.Test/TableFunctionTests.cs +++ b/DuckDB.NET.Test/TableFunctionTests.cs @@ -305,6 +305,41 @@ public void RegisterFunctionWithDateOnlyTimeOnlyParameters() data.Should().BeEquivalentTo([new DateTime(2024, 11, 6, 10, 30, 24)]); } + [Fact] + public void RegisterFunctionWithDuckDBDateOnlyTimeOnlyParametersAndColumns() + { + Connection.RegisterTableFunction("duckdb_date_time_rows", parameters => + { + var date = parameters[0].GetValue(); + var time = parameters[1].GetValue(); + + return new TableFunction(new List + { + new("d", typeof(DuckDBDateOnly)), + new("t", typeof(DuckDBTimeOnly)), + }, new[] { (date, time), (DuckDBDateOnly.PositiveInfinity, time) }); + }, (item, writers, rowIndex) => + { + var (date, time) = ((DuckDBDateOnly, DuckDBTimeOnly))item; + + writers[0].WriteValue(date, rowIndex); + writers[1].WriteValue(time, rowIndex); + }); + + Command.CommandText = "SELECT d::VARCHAR, t::VARCHAR, typeof(d), typeof(t) FROM duckdb_date_time_rows(DATE '2024-11-06', TIME '10:30:24.123456')"; + using var reader = Command.ExecuteReader(); + + reader.Read().Should().BeTrue(); + reader.GetString(0).Should().Be("2024-11-06"); + reader.GetString(1).Should().Be("10:30:24.123456"); + reader.GetString(2).Should().Be("DATE"); + reader.GetString(3).Should().Be("TIME"); + + reader.Read().Should().BeTrue(); + reader.GetString(0).Should().Be("infinity"); + reader.GetString(1).Should().Be("10:30:24.123456"); + } + [Fact] public void RegisterTableFunctionWithExplicitTimestampTypes() { From d70b2a6cfc253677db5f1bfd6fd1f49431565ed1 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 12:12:24 +0400 Subject: [PATCH 15/26] Stop the provider factory throwing when asked what it can create DuckDBClientFactory overrode CreateCommandBuilder and CreateDataAdapter to throw NotImplementedException. DbProviderFactory.CanCreateCommandBuilder and CanCreateDataAdapter work by calling those methods and checking for null, so the two properties threw too. Generic data code asks them precisely to avoid an exception. The overrides are removed. The base methods return null, as ADO.NET specifies for a provider without these features, and both properties now report false. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Data/DuckDBClientFactory.cs | 7 +++---- DuckDB.NET.Test/DuckDBClientFactoryTests.cs | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/DuckDB.NET.Data/DuckDBClientFactory.cs b/DuckDB.NET.Data/DuckDBClientFactory.cs index bb8c1f71..7dad7438 100644 --- a/DuckDB.NET.Data/DuckDBClientFactory.cs +++ b/DuckDB.NET.Data/DuckDBClientFactory.cs @@ -24,16 +24,15 @@ private DuckDBClientFactory() { } #region Methods - public override DbCommand CreateCommand() => new DuckDBCommand(); + // DuckDB.NET has no command builder or data adapter. CreateCommandBuilder and CreateDataAdapter are not + // overridden, so they return null and CanCreateCommandBuilder and CanCreateDataAdapter report false. - public override DbCommandBuilder CreateCommandBuilder() => throw new NotImplementedException(); + public override DbCommand CreateCommand() => new DuckDBCommand(); public override DbConnection CreateConnection() => new DuckDBConnection(); public override DbConnectionStringBuilder CreateConnectionStringBuilder() => new DuckDBConnectionStringBuilder(); - public override DbDataAdapter CreateDataAdapter() => throw new NotImplementedException(); - public override DbParameter CreateParameter() => new DuckDBParameter(); #endregion diff --git a/DuckDB.NET.Test/DuckDBClientFactoryTests.cs b/DuckDB.NET.Test/DuckDBClientFactoryTests.cs index 4d27ee2b..48c8ed91 100644 --- a/DuckDB.NET.Test/DuckDBClientFactoryTests.cs +++ b/DuckDB.NET.Test/DuckDBClientFactoryTests.cs @@ -47,4 +47,18 @@ public void UseFactory() Assert.Equal(42, value); } + + [Fact] + public void FactoryReportsWhatItCannotCreate() + { + DbProviderFactory factory = DuckDBClientFactory.Instance; + + // Generic data code asks these before creating anything, so they must answer, not throw. + Assert.False(factory.CanCreateDataAdapter); + Assert.False(factory.CanCreateCommandBuilder); + Assert.False(factory.CanCreateDataSourceEnumerator); + + Assert.Null(factory.CreateDataAdapter()); + Assert.Null(factory.CreateCommandBuilder()); + } } \ No newline at end of file From 957fee1e755a8d032199c639182494c7e0efe479 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Mon, 5 Oct 2026 12:23:01 +0400 Subject: [PATCH 16/26] Report a missing parameter name the same way through the base type Looking up a parameter by name through DbParameterCollection threw ArgumentOutOfRangeException ("Index was out of range") when the name did not exist, because GetParameter(string) used the index -1. The typed indexer, the setter and RemoveAt already threw IndexOutOfRangeException with "Parameter '' not found"; the getter now does too. New tests use the collection as DbParameterCollection, the way generic data code does: AddRange(Array), Insert, Contains, IndexOf, both indexers, CopyTo, enumeration and Remove. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Data/DuckDBParameterCollection.cs | 6 +- .../Parameters/ParameterCollectionTests.cs | 58 ++++++++++++++++++- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/DuckDB.NET.Data/DuckDBParameterCollection.cs b/DuckDB.NET.Data/DuckDBParameterCollection.cs index 86204812..d68c9134 100644 --- a/DuckDB.NET.Data/DuckDBParameterCollection.cs +++ b/DuckDB.NET.Data/DuckDBParameterCollection.cs @@ -85,11 +85,7 @@ public void CopyTo(DuckDBParameter[] array, int index) protected override DbParameter GetParameter(int index) => parameters[index]; - protected override DbParameter GetParameter(string parameterName) - { - var index = IndexOf(parameterName); - return parameters[index]; - } + protected override DbParameter GetParameter(string parameterName) => parameters[IndexOfSafe(parameterName)]; public override void AddRange(Array values) => AddRange(values.Cast()); diff --git a/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs b/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs index c4c2f896..786c93be 100644 --- a/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs +++ b/DuckDB.NET.Test/Parameters/ParameterCollectionTests.cs @@ -1,4 +1,5 @@ -using DuckDB.NET.Test.Helpers; +using System.Data.Common; +using DuckDB.NET.Test.Helpers; namespace DuckDB.NET.Test.Parameters; @@ -109,6 +110,61 @@ public void DuckDBParameterCollectionTests() parameters.Count.Should().Be(0); } + // Generic data code holds the collection as DbParameterCollection, which routes to the object-typed members. + [Fact] + public void ParameterCollectionWorksThroughTheBaseType() + { + DbParameterCollection parameters = new DuckDBParameterCollection(); + var first = new DuckDBParameter("first", 1); + var second = new DuckDBParameter("second", 2); + var third = new DuckDBParameter("third", 3); + + parameters.AddRange(new DbParameter[] { first, third }); + parameters.Insert(1, second); + + parameters.Count.Should().Be(3); + parameters.SyncRoot.Should().NotBeNull(); + parameters.Cast().Should().Equal(first, second, third); + + parameters.Contains((object)second).Should().BeTrue(); + parameters.Contains("second").Should().BeTrue(); + parameters.IndexOf((object)third).Should().Be(2); + parameters[1].Should().BeSameAs(second); + parameters["third"].Should().BeSameAs(third); + + var copy = new DuckDBParameter[4]; + parameters.CopyTo(copy, 1); + copy.Should().Equal(new DuckDBParameter[] { null, first, second, third }); + + var typedCopy = new DuckDBParameter[3]; + ((DuckDBParameterCollection)parameters).CopyTo(typedCopy, 0); + typedCopy.Should().Equal(first, second, third); + + var replacement = new DuckDBParameter("replacement", 4); + parameters[0] = replacement; + parameters["second"] = first; + parameters.Cast().Should().Equal(replacement, first, third); + + parameters.Remove((object)third); + parameters.Contains((object)third).Should().BeFalse(); + parameters.Count.Should().Be(2); + } + + [Fact] + public void MissingParameterNameIsReportedTheSameWayOnEveryPath() + { + var typedParameters = new DuckDBParameterCollection { new DuckDBParameter("present", 1) }; + DbParameterCollection parameters = typedParameters; + + parameters.Invoking(p => p["missing"]).Should().Throw().WithMessage("*'missing'*"); + parameters.Invoking(p => p["missing"] = new DuckDBParameter("other", 2)).Should().Throw().WithMessage("*'missing'*"); + parameters.Invoking(p => p.RemoveAt("missing")).Should().Throw().WithMessage("*'missing'*"); + typedParameters.Invoking(p => p["missing"]).Should().Throw().WithMessage("*'missing'*"); + + parameters.Contains("missing").Should().BeFalse(); + parameters.IndexOf("missing").Should().Be(-1); + } + [Theory] [InlineData("INSERT INTO ParametersTestKeyValue (KEY, VALUE) VALUES (?, ?)")] [InlineData("INSERT INTO ParametersTestKeyValue (KEY, VALUE) VALUES (?1, ?2)")] From e5a810231d6ced6c3870f10f51f8828c7ae0aa7f Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 6 Oct 2026 01:14:06 +0400 Subject: [PATCH 17/26] Show the new DuckLabs logo in the README Add ducklabs-logo.png to the sponsors folder and point the README's sponsor image at it instead of duckdb-labs-logo.png. The image URL reads from main, so the logo appears only after this change is merged there. --- .github/sponsors/ducklabs-logo.png | Bin 0 -> 105030 bytes README.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 .github/sponsors/ducklabs-logo.png diff --git a/.github/sponsors/ducklabs-logo.png b/.github/sponsors/ducklabs-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c57cbd64827d0272593ccf5c080081d6e07337a2 GIT binary patch literal 105030 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 35c9ea61..e77eb67e 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,6 @@ If you encounter a bug with the library [Create an Issue](https://github.com/Gio A big thanks to [DuckLabs](https://ducklabs.com/) and [AWS Open Source Software Fund](https://github.com/aws/dotnet-foss) for sponsoring the project! -[![DuckLabs](https://raw.githubusercontent.com/Giorgi/DuckDB.NET/main/.github/sponsors/duckdb-labs-logo.png)](https://ducklabs.com/) +[![DuckLabs](https://raw.githubusercontent.com/Giorgi/DuckDB.NET/main/.github/sponsors/ducklabs-logo.png)](https://ducklabs.com/) [![AWS](https://raw.githubusercontent.com/Giorgi/DuckDB.NET/main/.github/sponsors/aws-logo-small.png)](https://github.com/aws/dotnet-foss) From bb0c98c5fe97db5c8ae551ee75d3ddd1024ff51d Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 6 Oct 2026 12:53:10 +0400 Subject: [PATCH 18/26] Accept -2^127 as a HUGEINT value DuckDB's HUGEINT is a 128-bit two's complement integer, from -2^127 to 2^127 - 1. DuckDBHugeInt.HugeIntMinValue was -(2^127 - 1), so the constructor rejected -2^127: the value could be read from DuckDB but not written through a parameter or an appender. The minimum is now -2^127. The constructor needed no other change: for -2^127 the upper half is exactly -2^63 and the lower half is 0. SimpleNegativeHugeIntTest builds its SQL from the constant and needed parentheses, because a cast binds tighter than the minus sign and 2^127 itself is not a HUGEINT. A new test writes both ends of the range as parameters and through the appender, and checks that one step outside is rejected. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Bindings/DuckDBHugeInt.cs | 3 +- .../Parameters/HugeIntParameterTests.cs | 43 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/DuckDB.NET.Bindings/DuckDBHugeInt.cs b/DuckDB.NET.Bindings/DuckDBHugeInt.cs index 2215f5ca..f097910d 100644 --- a/DuckDB.NET.Bindings/DuckDBHugeInt.cs +++ b/DuckDB.NET.Bindings/DuckDBHugeInt.cs @@ -7,7 +7,8 @@ public readonly struct DuckDBHugeInt { private static readonly BigInteger Base = BigInteger.Pow(2, 64); - public static BigInteger HugeIntMinValue { get; } = BigInteger.Parse("-170141183460469231731687303715884105727"); + // DuckDB's HUGEINT is a 128-bit two's complement integer: -2^127 to 2^127 - 1. + public static BigInteger HugeIntMinValue { get; } = BigInteger.Parse("-170141183460469231731687303715884105728"); public static BigInteger HugeIntMaxValue { get; } = BigInteger.Parse("170141183460469231731687303715884105727"); public DuckDBHugeInt(BigInteger value) diff --git a/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs b/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs index 60ee0f16..fdf31e72 100644 --- a/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs +++ b/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs @@ -50,7 +50,8 @@ public void BindValueTest() [Fact] public void SimpleNegativeHugeIntTest() { - Command.CommandText = $"SELECT {DuckDBHugeInt.HugeIntMinValue}::HUGEINT;"; + // The parentheses matter: a cast binds tighter than the minus sign, and 2^127 itself is not a HUGEINT. + Command.CommandText = $"SELECT ({DuckDBHugeInt.HugeIntMinValue})::HUGEINT;"; Command.ExecuteNonQuery(); var scalar = Command.ExecuteScalar(); @@ -83,6 +84,46 @@ public void BindNegativeHugeIntValueTest() receivedValue.Should().Be(value); } + [Fact] + public void HugeIntMinimumAndMaximumRoundTrip() + { + // HUGEINT is a 128-bit two's complement integer: -2^127 to 2^127 - 1. + DuckDBHugeInt.HugeIntMinValue.Should().Be(-BigInteger.Pow(2, 127)); + DuckDBHugeInt.HugeIntMaxValue.Should().Be(BigInteger.Pow(2, 127) - 1); + + Command.CommandText = "CREATE OR REPLACE TABLE HugeIntRangeTests (value HUGEINT);"; + Command.ExecuteNonQuery(); + + BigInteger? minimum = DuckDBHugeInt.HugeIntMinValue; + BigInteger? maximum = DuckDBHugeInt.HugeIntMaxValue; + + // Once as parameters and once through the appender. + Command.CommandText = "INSERT INTO HugeIntRangeTests VALUES ($1), ($2);"; + Command.Parameters.Add(new DuckDBParameter(minimum.Value)); + Command.Parameters.Add(new DuckDBParameter(maximum.Value)); + Command.ExecuteNonQuery(); + Command.Parameters.Clear(); + + using (var appender = Connection.CreateAppender("HugeIntRangeTests")) + { + appender.CreateRow().AppendValue(minimum).EndRow(); + appender.CreateRow().AppendValue(maximum).EndRow(); + } + + Command.CommandText = "SELECT value::VARCHAR FROM HugeIntRangeTests ORDER BY rowid;"; + using var reader = Command.ExecuteReader(); + + foreach (var expected in new[] { minimum.Value, maximum.Value, minimum.Value, maximum.Value }) + { + reader.Read().Should().BeTrue(); + reader.GetString(0).Should().Be(expected.ToString()); + } + + // One step outside the range on either side is rejected. + FluentActions.Invoking(() => new DuckDBHugeInt(minimum.Value - 1)).Should().Throw(); + FluentActions.Invoking(() => new DuckDBHugeInt(maximum.Value + 1)).Should().Throw(); + } + [Fact] public void BindParameterWithoutTable_HugeInt() { From 241579b47f9fc67d2f73c7806a2ebd3d693faa63 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 6 Oct 2026 12:56:42 +0400 Subject: [PATCH 19/26] Bind BigInteger parameters for UHUGEINT columns as UHUGEINT A BigInteger parameter was always converted to a signed DuckDBHugeInt, so a value of 2^127 or more threw ArgumentOutOfRangeException even when DuckDB reported the parameter's type as UHUGEINT, for example when it was inserted into or compared with a UHUGEINT column. Such a parameter is now built as a DuckDBUHugeInt, which accepts 0 to 2^128 - 1 and rejects anything else. Parameters for which DuckDB reports no type, such as "$1::UHUGEINT", still go through the signed conversion. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- .../PreparedStatement/ClrToDuckDBConverter.cs | 1 + .../Parameters/HugeIntParameterTests.cs | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs index a2ada158..824d483f 100644 --- a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs +++ b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs @@ -72,6 +72,7 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log (DuckDBType.Decimal, decimal value) => DecimalToDuckDBValue(value), (DuckDBType.HugeInt, BigInteger value) => NativeMethods.Value.DuckDBCreateHugeInt(new DuckDBHugeInt(value)), + (DuckDBType.UnsignedHugeInt, BigInteger value) => NativeMethods.Value.DuckDBCreateUHugeInt(new DuckDBUHugeInt(value)), (DuckDBType.VarInt, BigInteger value) => BigIntegerToDuckDBValue(value), (DuckDBType.Varchar, string value) => NativeMethods.Value.DuckDBCreateVarchar(value), diff --git a/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs b/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs index fdf31e72..1334fc29 100644 --- a/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs +++ b/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs @@ -138,4 +138,46 @@ public void BindParameterWithoutTable_HugeInt() result.Should().BeOfType().Subject .Should().Be(value); } + + // 2^127 + 42 and 2^128 - 1 fit UHUGEINT but not HUGEINT. + public static IEnumerable UnsignedHugeIntValues() => + [ + [BigInteger.Zero], + [BigInteger.Pow(2, 127) + 42], + [BigInteger.Pow(2, 128) - 1], + ]; + + [Theory] + [MemberData(nameof(UnsignedHugeIntValues))] + public void BindBigIntegerToUnsignedHugeIntColumn(BigInteger value) + { + Command.CommandText = "CREATE OR REPLACE TABLE UHugeIntParameterTests (value UHUGEINT);"; + Command.ExecuteNonQuery(); + + Command.CommandText = "INSERT INTO UHugeIntParameterTests VALUES ($1);"; + Command.Parameters.Add(new DuckDBParameter(value)); + Command.ExecuteNonQuery(); + + // The parameter is compared with the UHUGEINT column, so DuckDB reports its type as UHUGEINT here too. + Command.CommandText = "SELECT value::VARCHAR FROM UHugeIntParameterTests WHERE value = $1;"; + + Command.ExecuteScalar().Should().Be(value.ToString()); + } + + [Fact] + public void BindOutOfRangeBigIntegerToUnsignedHugeIntColumnThrows() + { + Command.CommandText = "CREATE OR REPLACE TABLE UHugeIntParameterRangeTests (value UHUGEINT);"; + Command.ExecuteNonQuery(); + + Command.CommandText = "INSERT INTO UHugeIntParameterRangeTests VALUES ($1);"; + + foreach (var value in new[] { BigInteger.MinusOne, BigInteger.Pow(2, 128) }) + { + Command.Parameters.Clear(); + Command.Parameters.Add(new DuckDBParameter(value)); + + Command.Invoking(command => command.ExecuteNonQuery()).Should().Throw(); + } + } } \ No newline at end of file From 3b6f1a4eeb55a8ea2d9be21ab1f50a739d3b81b7 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 6 Oct 2026 13:18:42 +0400 Subject: [PATCH 20/26] Bind a BigInteger with no target type as the smallest type that fits When DuckDB reports no type for a parameter, as in "SELECT $1" or "$1::UHUGEINT", a BigInteger was always converted to a signed DuckDBHugeInt. A value outside the HUGEINT range threw ArgumentOutOfRangeException before the query ran, even when the query cast it to UHUGEINT. Such a parameter is now bound as HUGEINT if the value fits, as UHUGEINT for values up to 2^128 - 1, and as BIGNUM beyond that. Values that fit HUGEINT bind exactly as before. This is a behaviour change for larger values: they used to throw and are now sent to DuckDB, which applies any cast in the query. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- .../PreparedStatement/ClrToDuckDBConverter.cs | 19 +++++++++- .../Parameters/HugeIntParameterTests.cs | 36 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs index 824d483f..b3f17372 100644 --- a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs +++ b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs @@ -18,7 +18,7 @@ internal static class ClrToDuckDBConverter { DbType.Single, value => NativeMethods.Value.DuckDBCreateFloat((float)value) }, { DbType.Double, value => NativeMethods.Value.DuckDBCreateDouble((double)value) }, { DbType.String, value => NativeMethods.Value.DuckDBCreateVarchar((string?)value) }, - { DbType.VarNumeric, value => NativeMethods.Value.DuckDBCreateHugeInt(new((BigInteger)value)) }, + { DbType.VarNumeric, value => BigIntegerWithoutTargetType((BigInteger)value) }, { DbType.Binary, value => { var bytes = (byte[])value; @@ -161,6 +161,23 @@ private static DuckDBValue[] BuildValues(DuckDBLogicalType childType, ICollectio return values; } + // DuckDB reported no type for the parameter, for example in "SELECT $1" or "$1::UHUGEINT", so pick the smallest + // type that holds the value exactly: HUGEINT, then UHUGEINT, then BIGNUM. + private static DuckDBValue BigIntegerWithoutTargetType(BigInteger value) + { + if (value >= DuckDBHugeInt.HugeIntMinValue && value <= DuckDBHugeInt.HugeIntMaxValue) + { + return NativeMethods.Value.DuckDBCreateHugeInt(new DuckDBHugeInt(value)); + } + + if (value >= DuckDBUHugeInt.HugeIntMinValue && value <= DuckDBUHugeInt.HugeIntMaxValue) + { + return NativeMethods.Value.DuckDBCreateUHugeInt(new DuckDBUHugeInt(value)); + } + + return BigIntegerToDuckDBValue(value); + } + private static unsafe DuckDBValue BigIntegerToDuckDBValue(BigInteger value) { // A BIGNUM is built from the magnitude bytes and a sign, which keeps values of any size exact. diff --git a/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs b/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs index 1334fc29..ff583bff 100644 --- a/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs +++ b/DuckDB.NET.Test/Parameters/HugeIntParameterTests.cs @@ -138,7 +138,6 @@ public void BindParameterWithoutTable_HugeInt() result.Should().BeOfType().Subject .Should().Be(value); } - // 2^127 + 42 and 2^128 - 1 fit UHUGEINT but not HUGEINT. public static IEnumerable UnsignedHugeIntValues() => [ @@ -180,4 +179,39 @@ public void BindOutOfRangeBigIntegerToUnsignedHugeIntColumnThrows() Command.Invoking(command => command.ExecuteNonQuery()).Should().Throw(); } } + + // With a cast or no context, DuckDB reports no parameter type and the value picks its own type. + [Theory] + [InlineData("SELECT ($1::UHUGEINT)::VARCHAR;")] + [InlineData("SELECT $1::VARCHAR;")] + public void BindBigIntegerAboveHugeIntRangeWithoutColumn(string query) + { + var value = BigInteger.Pow(2, 127) + 42; + + Command.CommandText = query; + Command.Parameters.Add(new DuckDBParameter(value)); + + Command.ExecuteScalar().Should().Be(value.ToString()); + } + + [Theory] + [InlineData("170141183460469231731687303715884105727", "HUGEINT")] + [InlineData("-170141183460469231731687303715884105728", "HUGEINT")] + [InlineData("170141183460469231731687303715884105728", "UHUGEINT")] + [InlineData("340282366920938463463374607431768211455", "UHUGEINT")] + [InlineData("340282366920938463463374607431768211456", "BIGNUM")] + [InlineData("-170141183460469231731687303715884105729", "BIGNUM")] + public void BigIntegerWithoutColumnPicksTheSmallestTypeThatFits(string text, string expectedType) + { + var value = BigInteger.Parse(text); + + Command.CommandText = "SELECT typeof($1), $1::VARCHAR;"; + Command.Parameters.Add(new DuckDBParameter(value)); + + using var reader = Command.ExecuteReader(); + reader.Read().Should().BeTrue(); + + reader.GetString(0).Should().Be(expectedType); + reader.GetString(1).Should().Be(text); + } } \ No newline at end of file From 6711ee6a427695038badfac10bb84309e9fc7549 Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 6 Oct 2026 13:59:14 +0400 Subject: [PATCH 21/26] Accept a BigInteger property for UHUGEINT columns in the mapped appender The validation required the single DuckDB type a .NET type maps to, so a BigInteger property was accepted only for HUGEINT. It now checks the column against the types the property can be appended to: HUGEINT and UHUGEINT for BigInteger, the mapped type for everything else. The range is checked per value when it is appended. The mismatch error names every accepted type and prints the underlying type of a nullable property. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Data/DuckDBMappedAppender.cs | 22 ++- DuckDB.NET.Test/DuckDBMappedAppenderTests.cs | 147 +++++++++++++++++++ 2 files changed, 163 insertions(+), 6 deletions(-) diff --git a/DuckDB.NET.Data/DuckDBMappedAppender.cs b/DuckDB.NET.Data/DuckDBMappedAppender.cs index 25a3444f..14e0af86 100644 --- a/DuckDB.NET.Data/DuckDBMappedAppender.cs +++ b/DuckDB.NET.Data/DuckDBMappedAppender.cs @@ -42,12 +42,12 @@ internal DuckDBMappedAppender(DuckDBAppender appender) } var columnType = NativeMethods.LogicalType.DuckDBGetTypeId(columnTypes[index]); - var expectedType = GetExpectedDuckDBType(mapping.PropertyType); + var acceptedTypes = GetAcceptedDuckDBTypes(mapping.PropertyType); - if (expectedType != columnType) + if (Array.IndexOf(acceptedTypes, columnType) < 0) { throw new InvalidOperationException( - $"Type mismatch at column index {index}: Mapped type is {mapping.PropertyType.Name} (expected DuckDB type: {expectedType}) but actual column type is {columnType}"); + $"Type mismatch at column index {index}: Mapped type is {mapping.PropertyType.UnderlyingTypeOrSelf().Name} (expected DuckDB type: {string.Join(" or ", acceptedTypes)}) but actual column type is {columnType}"); } } } @@ -86,14 +86,24 @@ private void AppendRecord(T record) }); } - private static DuckDBType GetExpectedDuckDBType(Type type) + // The column types a property of the given type can be appended to. + private static DuckDBType[] GetAcceptedDuckDBTypes(Type type) { - var duckDBType = type.UnderlyingTypeOrSelf().GetDuckDBType(); + var underlyingType = type.UnderlyingTypeOrSelf(); + + // BigInteger is the .NET type for both 128-bit integer columns. The writer stores the value signed or + // unsigned to match the column, and rejects a value outside the column's range when it is appended. + if (underlyingType == typeof(BigInteger)) + { + return [DuckDBType.HugeInt, DuckDBType.UnsignedHugeInt]; + } + + var duckDBType = underlyingType.GetDuckDBType(); return duckDBType switch { DuckDBType.Invalid => throw new NotSupportedException($"Type {type.Name} is not supported for mapping"), - _ => duckDBType + _ => [duckDBType] }; } diff --git a/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs b/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs index 0815a27d..7dd93e4f 100644 --- a/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs +++ b/DuckDB.NET.Test/DuckDBMappedAppenderTests.cs @@ -386,4 +386,151 @@ public void MappedAppender_SupportsDuckDBDateOnlyAndTimeOnly() reader.GetFieldValue(1).Should().Be(records[2].Time); reader.GetString(4).Should().Be("-infinity"); } + + public class BigNumbers + { + public BigInteger Signed { get; set; } + public BigInteger Unsigned { get; set; } + } + + public class BigNumbersMap : DuckDBAppenderMap + { + public BigNumbersMap() + { + Map(x => x.Signed); + Map(x => x.Unsigned); + } + } + + public class NullableBigNumbers + { + public BigInteger? Signed { get; set; } + public BigInteger? Unsigned { get; set; } + } + + public class NullableBigNumbersMap : DuckDBAppenderMap + { + public NullableBigNumbersMap() + { + Map(x => x.Signed); + Map(x => x.Unsigned); + } + } + + [Fact] + public void MappedAppender_SupportsBigIntegerForUnsignedHugeInt() + { + Command.CommandText = "CREATE TABLE mapped_big_numbers(signed_value HUGEINT, unsigned_value UHUGEINT);"; + Command.ExecuteNonQuery(); + + // The second and third unsigned values do not fit a signed HUGEINT. + var records = new[] + { + new BigNumbers { Signed = -5, Unsigned = 5 }, + new BigNumbers { Signed = BigInteger.Pow(2, 126), Unsigned = BigInteger.Pow(2, 127) + 42 }, + new BigNumbers { Signed = DuckDBHugeInt.HugeIntMinValue, Unsigned = BigInteger.Pow(2, 128) - 1 }, + new BigNumbers { Signed = DuckDBHugeInt.HugeIntMaxValue, Unsigned = 0 }, + }; + + using (var appender = Connection.CreateAppender("mapped_big_numbers")) + { + appender.AppendRecords(records); + } + + Command.CommandText = "SELECT signed_value::VARCHAR, unsigned_value::VARCHAR FROM mapped_big_numbers ORDER BY rowid"; + using var reader = Command.ExecuteReader(); + + foreach (var record in records) + { + reader.Read().Should().BeTrue(); + reader.GetString(0).Should().Be(record.Signed.ToString()); + reader.GetString(1).Should().Be(record.Unsigned.ToString()); + } + } + + [Fact] + public void MappedAppender_RejectsNegativeBigIntegerForUnsignedHugeInt() + { + Command.CommandText = "CREATE TABLE mapped_big_numbers_negative(signed_value HUGEINT, unsigned_value UHUGEINT);"; + Command.ExecuteNonQuery(); + + using var appender = Connection.CreateAppender("mapped_big_numbers_negative"); + + appender.Invoking(a => a.AppendRecords(new[] { new BigNumbers { Signed = 1, Unsigned = -1 } })) + .Should().Throw(); + } + + [Fact] + public void MappedAppender_RejectsBigIntegerAboveUnsignedHugeIntRange() + { + Command.CommandText = "CREATE TABLE mapped_big_numbers_too_big(signed_value HUGEINT, unsigned_value UHUGEINT);"; + Command.ExecuteNonQuery(); + + using var appender = Connection.CreateAppender("mapped_big_numbers_too_big"); + + appender.Invoking(a => a.AppendRecords(new[] { new BigNumbers { Signed = 1, Unsigned = BigInteger.Pow(2, 128) } })) + .Should().Throw(); + } + + [Fact] + public void MappedAppender_SupportsNullableBigIntegerForUnsignedHugeInt() + { + Command.CommandText = "CREATE TABLE mapped_nullable_big_numbers(signed_value HUGEINT, unsigned_value UHUGEINT);"; + Command.ExecuteNonQuery(); + + var records = new[] + { + new NullableBigNumbers { Signed = -5, Unsigned = BigInteger.Pow(2, 127) + 42 }, + new NullableBigNumbers { Signed = null, Unsigned = null }, + new NullableBigNumbers { Signed = null, Unsigned = BigInteger.Pow(2, 128) - 1 }, + }; + + using (var appender = Connection.CreateAppender("mapped_nullable_big_numbers")) + { + appender.AppendRecords(records); + } + + Command.CommandText = "SELECT signed_value::VARCHAR, unsigned_value::VARCHAR FROM mapped_nullable_big_numbers ORDER BY rowid"; + using var reader = Command.ExecuteReader(); + + foreach (var record in records) + { + reader.Read().Should().BeTrue(); + reader.IsDBNull(0).Should().Be(record.Signed == null); + reader.IsDBNull(1).Should().Be(record.Unsigned == null); + + if (record.Signed != null) + { + reader.GetString(0).Should().Be(record.Signed.ToString()); + } + + if (record.Unsigned != null) + { + reader.GetString(1).Should().Be(record.Unsigned.ToString()); + } + } + } + + [Fact] + public void MappedAppender_NamesBothColumnTypesAcceptedForBigInteger() + { + Command.CommandText = "CREATE TABLE mapped_big_numbers_mismatch(signed_value HUGEINT, unsigned_value VARCHAR);"; + Command.ExecuteNonQuery(); + + Connection.Invoking(c => c.CreateAppender("mapped_big_numbers_mismatch")) + .Should().Throw() + .WithMessage("*column index 1*BigInteger*HugeInt or UnsignedHugeInt*Varchar*"); + } + + // The appender has no writer for BIGNUM, so the column is rejected instead of being written with the wrong layout. + [Fact] + public void MappedAppender_RejectsBigIntegerForBigNumColumn() + { + Command.CommandText = "CREATE TABLE mapped_big_numbers_bignum(signed_value HUGEINT, unsigned_value BIGNUM);"; + Command.ExecuteNonQuery(); + + Connection.Invoking(c => c.CreateAppender("mapped_big_numbers_bignum")) + .Should().Throw() + .WithMessage("*column index 1*Mapped type is BigInteger *HugeInt or UnsignedHugeInt*"); + } } From 56b8941ff04ded50270c9f1aaa6d7dbafb1d70dd Mon Sep 17 00:00:00 2001 From: Giorgi Dalakishvili Date: Tue, 6 Oct 2026 14:12:57 +0400 Subject: [PATCH 22/26] Dispose the appender when mapped appender validation fails CreateAppender created the low-level appender and passed it to the DuckDBMappedAppender constructor. When the constructor threw (no mappings, wrong column count, type mismatch), nothing disposed the appender, and its native objects stayed alive until the finalizer ran. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01CzLKjBGAwccxYB6ny5bpmY --- DuckDB.NET.Data/DuckDBConnection.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/DuckDB.NET.Data/DuckDBConnection.cs b/DuckDB.NET.Data/DuckDBConnection.cs index 9f20e8ad..1d350dcf 100644 --- a/DuckDB.NET.Data/DuckDBConnection.cs +++ b/DuckDB.NET.Data/DuckDBConnection.cs @@ -217,7 +217,17 @@ public DuckDBMappedAppender CreateAppender(string? catalog, st where TMap : Mapping.DuckDBAppenderMap, new() { var appender = CreateAppender(catalog, schema, table); - return new DuckDBMappedAppender(appender); + + try + { + return new DuckDBMappedAppender(appender); + } + catch + { + // The mapping did not pass validation, so nobody else gets to dispose the appender. + appender.Dispose(); + throw; + } } protected override void Dispose(bool disposing) From 136c4eb45ad47c076af1571572d932830713cab4 Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Tue, 6 Oct 2026 09:21:34 -0500 Subject: [PATCH 23/26] Read and write negative intervals DuckDB's interval micros is signed (duckdb_interval.micros is int64_t) and days can be negative, but DuckDBInterval converted both as unsigned: reading any negative interval as a TimeSpan threw ArgumentOutOfRangeException ("INTERVAL '-90 minutes'", even "-2 days"), a negative month was silently dropped instead of refused, and binding or appending a negative TimeSpan threw OverflowException. Micros stays a ulong for compatibility and is read and written as its two's complement. A non-zero month count, either sign, is refused, and the range check is done in Int128. Co-Authored-By: Claude Opus 5.5 --- DuckDB.NET.Bindings/DuckDBInterval.cs | 43 +++-------- DuckDB.NET.Test/DuckDBIntervalTests.cs | 44 ++++++++++++ DuckDB.NET.Test/Parameters/IntervalTests.cs | 79 +++++++++++++++++++++ 3 files changed, 132 insertions(+), 34 deletions(-) create mode 100644 DuckDB.NET.Test/Parameters/IntervalTests.cs diff --git a/DuckDB.NET.Bindings/DuckDBInterval.cs b/DuckDB.NET.Bindings/DuckDBInterval.cs index ec832c2d..ebb48c94 100644 --- a/DuckDB.NET.Bindings/DuckDBInterval.cs +++ b/DuckDB.NET.Bindings/DuckDBInterval.cs @@ -5,7 +5,6 @@ namespace DuckDB.NET.Native; [StructLayout(LayoutKind.Sequential)] public readonly struct DuckDBInterval(int months, int days, ulong micros) { - private const ulong MillisecondsByDay = (ulong)(24 * 60 * 60 * 1e6); public int Months { get; } = months; public int Days { get; } = days; @@ -24,48 +23,24 @@ public bool TryConvert([NotNullWhen(true)] out TimeSpan? timeSpan) return exception is null; } + // DuckDB's micros is signed (duckdb_interval.micros is int64_t), and Days can be negative too. Micros + // exposes the value as ulong, so it is read and written as its two's complement. private static (TimeSpan?, Exception?) ToTimeSpan(DuckDBInterval interval) { - if (interval.Months > 0) + if (interval.Months != 0) { - return (null, new ArgumentOutOfRangeException(nameof(interval), $"Cannot convert a value of type {nameof(DuckDBInterval)} to type {nameof(TimeSpan)} when the attribute 'Months' is greater or equal to 1")); + return (null, new ArgumentOutOfRangeException(nameof(interval), $"Cannot convert a value of type {nameof(DuckDBInterval)} to type {nameof(TimeSpan)} when the attribute 'Months' is not 0")); } - var days = 0; - var micros = interval.Micros; - - if (interval.Micros >= MillisecondsByDay) - { - days = Convert.ToInt32(Math.Floor((double)(interval.Micros / MillisecondsByDay))); - if (days > int.MaxValue - interval.Days) - { - return (null, new ArgumentOutOfRangeException(nameof(interval), $"Cannot convert a value of type {nameof(DuckDBInterval)} to type {nameof(TimeSpan)} when the total days value is larger than {int.MaxValue}")); - } - - if (days > 0) - { - micros = interval.Micros - ((ulong)days * MillisecondsByDay); - } - days = interval.Days + days; - } - else - { - days = interval.Days; - } - - if (micros * 10 > long.MaxValue) - { - return (null, new ArgumentOutOfRangeException(nameof(interval), $"Cannot convert a value of type {nameof(DuckDBInterval)} to type {nameof(TimeSpan)} when the value of microseconds is larger than {long.MaxValue / 10}")); - } - - if ((ulong)days * MillisecondsByDay * 100 + micros * 10 > long.MaxValue) + var ticks = (Int128)interval.Days * TimeSpan.TicksPerDay + (Int128)unchecked((long)interval.Micros) * TimeSpan.TicksPerMicrosecond; + if (ticks > long.MaxValue || ticks < long.MinValue) { - return (null, new ArgumentOutOfRangeException(nameof(interval), $"Cannot convert a value of type {nameof(DuckDBInterval)} to type {nameof(TimeSpan)} when the value of total microseconds is larger than {long.MaxValue}")); + return (null, new ArgumentOutOfRangeException(nameof(interval), $"Cannot convert a value of type {nameof(DuckDBInterval)} to type {nameof(TimeSpan)} when the total value is outside the range of {nameof(TimeSpan)}")); } - return (new TimeSpan(days, 0, 0, 0) + new TimeSpan((long)micros * 10), null); + return (new TimeSpan((long)ticks), null); } private static DuckDBInterval FromTimeSpan(TimeSpan timeSpan) - => new(0, timeSpan.Days, Convert.ToUInt64(timeSpan.Ticks / 10 - new TimeSpan(timeSpan.Days, 0, 0, 0).Ticks / 10)); + => new(0, timeSpan.Days, unchecked((ulong)(timeSpan.Ticks % TimeSpan.TicksPerDay / TimeSpan.TicksPerMicrosecond))); } \ No newline at end of file diff --git a/DuckDB.NET.Test/DuckDBIntervalTests.cs b/DuckDB.NET.Test/DuckDBIntervalTests.cs index cd0cbc0e..6f80ca4d 100644 --- a/DuckDB.NET.Test/DuckDBIntervalTests.cs +++ b/DuckDB.NET.Test/DuckDBIntervalTests.cs @@ -47,6 +47,50 @@ public void ToTimeSpan_TooBigDays_Exception() Assert.Throws(() => (TimeSpan)interval); } + // DuckDB stores an interval's micros as a signed 64-bit value (duckdb_interval.micros is int64_t). + [Theory] + [InlineData(0, -5_400_000_000L, "-01:30:00")] + [InlineData(0, -17L, "-00:00:00.000017")] + [InlineData(-1, -3_600_000_000L, "-1.01:00:00")] + [InlineData(1, -3_600_000_000L, "23:00:00")] + [InlineData(-2, 0L, "-2.00:00:00")] + [InlineData(-1, 25 * 3_600_000_000L, "01:00:00")] + public void ToTimeSpan_NegativeValue_ExpectedResult(int days, long micros, string ts) + { + var interval = new DuckDBInterval(0, days, unchecked((ulong)micros)); + Assert.True(interval.TryConvert(out var timeSpan)); + Assert.Equal(TimeSpan.Parse(ts), timeSpan); + Assert.Equal(TimeSpan.Parse(ts), (TimeSpan)interval); + } + + [Fact] + public void ToTimeSpan_NegativeMonthInterval_Exception() + { + var interval = new DuckDBInterval(-1, 0, 0); + Assert.False(interval.TryConvert(out var ts)); + Assert.Throws(() => (TimeSpan)interval); + } + + [Fact] + public void ToTimeSpan_TooSmall_Exception() + { + var interval = new DuckDBInterval(0, int.MinValue, unchecked((ulong)long.MinValue)); + Assert.False(interval.TryConvert(out var ts)); + Assert.Throws(() => (TimeSpan)interval); + } + + [Theory] + [InlineData("-01:30:00", 0, -5_400_000_000L)] + [InlineData("-1.01:00:00", -1, -3_600_000_000L)] + [InlineData("-00:00:00.0000017", 0, -1L)] + public void ToDuckDBInterval_NegativeValue_ExpectedResult(string ts, int days, long micros) + { + DuckDBInterval interval = TimeSpan.Parse(ts); + Assert.Equal(0, interval.Months); + Assert.Equal(days, interval.Days); + Assert.Equal(micros, unchecked((long)interval.Micros)); + } + [Fact] public void ToDuckDBInterval_ValidValue_ExpectedResult() { diff --git a/DuckDB.NET.Test/Parameters/IntervalTests.cs b/DuckDB.NET.Test/Parameters/IntervalTests.cs new file mode 100644 index 00000000..00742a85 --- /dev/null +++ b/DuckDB.NET.Test/Parameters/IntervalTests.cs @@ -0,0 +1,79 @@ +namespace DuckDB.NET.Test.Parameters; + +// DuckDB intervals can be negative: duckdb_interval.micros is a signed 64-bit value. +public class IntervalTests(DuckDBDatabaseFixture db) : DuckDBTestBase(db) +{ + [Theory] + [InlineData("-90 minutes", -90 * 60)] + [InlineData("-1 day", -86_400)] + [InlineData("-1 day 2 hours", -79_200)] + [InlineData("1 day -2 hours", 79_200)] + public void QueryNegativeInterval(string interval, int seconds) + { + Command.CommandText = $"SELECT INTERVAL '{interval}';"; + + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetValue(0).Should().Be(TimeSpan.FromSeconds(seconds)); + reader.GetFieldValue(0).Should().Be(TimeSpan.FromSeconds(seconds)); + } + + [Theory] + [InlineData("-1 month")] + [InlineData("1 month")] + public void QueryMonthInterval_AsTimeSpan_Throws(string interval) + { + Command.CommandText = $"SELECT INTERVAL '{interval}';"; + + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.Invoking(r => r.GetFieldValue(0)).Should().Throw(); + reader.GetFieldValue(0).Months.Should().Be(interval.StartsWith('-') ? -1 : 1); + } + + [Theory] + [InlineData(-5_400_000_000L)] + [InlineData(-1L)] + [InlineData(-90_061_000_001L)] + public void InsertNegativeTimeSpanParameter(long micros) + { + var expected = TimeSpan.FromTicks(micros * 10); + + Command.CommandText = "CREATE OR REPLACE TABLE NegativeIntervalParameterTable (v INTERVAL);"; + Command.ExecuteNonQuery(); + + Command.CommandText = "INSERT INTO NegativeIntervalParameterTable (v) VALUES (?);"; + Command.Parameters.Add(new DuckDBParameter(expected)); + Command.ExecuteNonQuery(); + Command.Parameters.Clear(); + + Command.CommandText = $"SELECT v, v = CAST('{micros} microseconds' AS INTERVAL) FROM NegativeIntervalParameterTable;"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(expected); + reader.GetBoolean(1).Should().BeTrue(); + } + + [Fact] + public void AppendNegativeTimeSpan() + { + Command.CommandText = "CREATE TABLE NegativeIntervalTable (v INTERVAL);"; + Command.ExecuteNonQuery(); + var expected = TimeSpan.FromMinutes(-90).Add(TimeSpan.FromTicks(-10)); + + using (var appender = Connection.CreateAppender("NegativeIntervalTable")) + { + appender.CreateRow().AppendValue((TimeSpan?)expected).EndRow(); + } + + Command.CommandText = "SELECT v, v = INTERVAL '-5400000001 microseconds' FROM NegativeIntervalTable;"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(expected); + reader.GetBoolean(1).Should().BeTrue(); + } +} From b84b0a8c5c7d66935ed38cb6f4870346753442bf Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Tue, 6 Oct 2026 09:49:20 -0500 Subject: [PATCH 24/26] Bind a TimeSpan parameter with no inferred type as an interval TimeSpan has no DbType, so where DuckDB can't infer a parameter's type (a bare "?") it was sent as TimeSpan.ToString(): it came back as a string, and DuckDB can't parse that text as an interval once there are days ("1.01:01:01"). It is now bound as an INTERVAL there. Where DuckDB knows the type (an INTERVAL, TIME or VARCHAR target) nothing changes. Co-Authored-By: Claude Opus 5.5 --- .../PreparedStatement/ClrToDuckDBConverter.cs | 3 ++ DuckDB.NET.Test/Parameters/IntervalTests.cs | 43 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs index b3f17372..5d70510c 100644 --- a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs +++ b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs @@ -85,6 +85,9 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log (DuckDBType.TimestampTz, DateTime value) => NativeMethods.Value.DuckDBCreateTimestampTz(value.ToTimestampStruct(duckDBType)), (DuckDBType.TimestampTz, DateTimeOffset value) => NativeMethods.Value.DuckDBCreateTimestampTz(value.ToTimestampStruct()), (DuckDBType.Interval, TimeSpan value) => NativeMethods.Value.DuckDBCreateInterval(value), + // No inferred type (a bare "?"): TimeSpan has no DbType, and its ToString() isn't interval text + // DuckDB can parse once there are days ("1.01:01:01"). + (DuckDBType.Invalid, TimeSpan value) => NativeMethods.Value.DuckDBCreateInterval(value), (DuckDBType.Date, DateTime value) => NativeMethods.Value.DuckDBCreateDate(((DuckDBDateOnly)value).ToDuckDBDate()), (DuckDBType.Date, DuckDBDateOnly value) => NativeMethods.Value.DuckDBCreateDate(value.ToDuckDBDate()), (DuckDBType.Time, DateTime value) => NativeMethods.Value.DuckDBCreateTime(NativeMethods.DateTimeHelpers.DuckDBToTime((DuckDBTimeOnly)value)), diff --git a/DuckDB.NET.Test/Parameters/IntervalTests.cs b/DuckDB.NET.Test/Parameters/IntervalTests.cs index 00742a85..b2e10288 100644 --- a/DuckDB.NET.Test/Parameters/IntervalTests.cs +++ b/DuckDB.NET.Test/Parameters/IntervalTests.cs @@ -76,4 +76,47 @@ public void AppendNegativeTimeSpan() reader.GetFieldValue(0).Should().Be(expected); reader.GetBoolean(1).Should().BeTrue(); } + + // A TimeSpan has no DbType, so where DuckDB can't infer the parameter's type (a bare "?") it was + // sent as TimeSpan.ToString(), which DuckDB can't parse as an interval once there are days + // ("1.01:01:01"), and which came back as a string. + [Theory] + [InlineData(90_061_000_001L)] + [InlineData(-90_061_000_001L)] + [InlineData(5_400_000_000L)] + public void BindTimeSpanWithoutTargetType(long micros) + { + var expected = TimeSpan.FromTicks(micros * 10); + + Command.CommandText = "SELECT ?::INTERVAL, ?;"; + Command.Parameters.Add(new DuckDBParameter(expected)); + Command.Parameters.Add(new DuckDBParameter(expected)); + + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(expected); + reader.GetValue(1).Should().Be(expected); + } + + // A TimeSpan bound where DuckDB knows the parameter's type keeps its existing behavior. + [Fact] + public void BindTimeSpanIntoTimeAndVarcharColumns() + { + Command.CommandText = "CREATE OR REPLACE TABLE TimeSpanTargets (t TIME, s VARCHAR);"; + Command.ExecuteNonQuery(); + + Command.CommandText = "INSERT INTO TimeSpanTargets (t, s) VALUES (?, ?);"; + Command.Parameters.Add(new DuckDBParameter(new TimeSpan(0, 13, 45, 30, 125))); + Command.Parameters.Add(new DuckDBParameter(new TimeSpan(0, 1, 30, 0))); + Command.ExecuteNonQuery(); + Command.Parameters.Clear(); + + Command.CommandText = "SELECT t, s FROM TimeSpanTargets;"; + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetFieldValue(0).Should().Be(new TimeOnly(13, 45, 30, 125)); + reader.GetString(1).Should().Be("01:30:00"); + } } From a1a1232a94b3b35775e1812588eafa21b5d9dc6c Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Tue, 6 Oct 2026 10:21:33 -0500 Subject: [PATCH 25/26] Send a TimeSpan declared DbType.Time as a time Where DuckDB can't infer the type and the value ends up as a TIME (INSERT ... SELECT ?, a UNION with a TIME, ?::TIME::VARCHAR), the new interval default fails, since DuckDB has no INTERVAL -> TIME cast. As in Npgsql, a caller who wants a time declares DbType.Time: the interval arm stays out of the way, and the DbType.Time value creator accepts a TimeSpan through TimeOnly.FromTimeSpan, which refuses a negative value or one of a day or more (it threw InvalidCastException before). Co-Authored-By: Claude Opus 5.5 --- .../PreparedStatement/ClrToDuckDBConverter.cs | 12 +++-- DuckDB.NET.Test/Parameters/IntervalTests.cs | 49 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs index 5d70510c..d322521f 100644 --- a/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs +++ b/DuckDB.NET.Data/PreparedStatement/ClrToDuckDBConverter.cs @@ -33,7 +33,13 @@ internal static class ClrToDuckDBConverter }, { DbType.Time, value => { - var time = NativeMethods.DateTimeHelpers.DuckDBToTime(value is TimeOnly timeOnly ? (DuckDBTimeOnly)timeOnly : (DuckDBTimeOnly)value); + // A TimeSpan is a time of day here: TimeOnly.FromTimeSpan refuses a negative one or one of a day or more. + var time = NativeMethods.DateTimeHelpers.DuckDBToTime(value switch + { + TimeOnly timeOnly => (DuckDBTimeOnly)timeOnly, + TimeSpan timeSpan => (DuckDBTimeOnly)TimeOnly.FromTimeSpan(timeSpan), + _ => (DuckDBTimeOnly)value + }); return NativeMethods.Value.DuckDBCreateTime(time); } }, @@ -86,8 +92,8 @@ public static DuckDBValue ToDuckDBValue(this object? item, DuckDBLogicalType log (DuckDBType.TimestampTz, DateTimeOffset value) => NativeMethods.Value.DuckDBCreateTimestampTz(value.ToTimestampStruct()), (DuckDBType.Interval, TimeSpan value) => NativeMethods.Value.DuckDBCreateInterval(value), // No inferred type (a bare "?"): TimeSpan has no DbType, and its ToString() isn't interval text - // DuckDB can parse once there are days ("1.01:01:01"). - (DuckDBType.Invalid, TimeSpan value) => NativeMethods.Value.DuckDBCreateInterval(value), + // DuckDB can parse once there are days ("1.01:01:01"). DbType.Time sends it as a time instead. + (DuckDBType.Invalid, TimeSpan value) when dbType != DbType.Time => NativeMethods.Value.DuckDBCreateInterval(value), (DuckDBType.Date, DateTime value) => NativeMethods.Value.DuckDBCreateDate(((DuckDBDateOnly)value).ToDuckDBDate()), (DuckDBType.Date, DuckDBDateOnly value) => NativeMethods.Value.DuckDBCreateDate(value.ToDuckDBDate()), (DuckDBType.Time, DateTime value) => NativeMethods.Value.DuckDBCreateTime(NativeMethods.DateTimeHelpers.DuckDBToTime((DuckDBTimeOnly)value)), diff --git a/DuckDB.NET.Test/Parameters/IntervalTests.cs b/DuckDB.NET.Test/Parameters/IntervalTests.cs index b2e10288..08e8c8f4 100644 --- a/DuckDB.NET.Test/Parameters/IntervalTests.cs +++ b/DuckDB.NET.Test/Parameters/IntervalTests.cs @@ -119,4 +119,53 @@ public void BindTimeSpanIntoTimeAndVarcharColumns() reader.GetFieldValue(0).Should().Be(new TimeOnly(13, 45, 30, 125)); reader.GetString(1).Should().Be("01:30:00"); } + + // As in Npgsql, a caller who wants a TimeSpan sent as a time declares DbType.Time: where DuckDB can't + // infer the type, an interval has no cast to TIME. + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(13, 45, 30, 125)] + [InlineData(23, 59, 59, 999)] + public void InsertSelectTimeSpanDeclaredTime(int hours, int minutes, int seconds, int milliseconds) + { + var value = new TimeSpan(0, hours, minutes, seconds, milliseconds); + + Command.CommandText = "CREATE OR REPLACE TABLE TimeSpanDeclaredTime (t TIME);"; + Command.ExecuteNonQuery(); + + Command.CommandText = "INSERT INTO TimeSpanDeclaredTime (t) SELECT ?;"; + Command.Parameters.Add(new DuckDBParameter(DbType.Time, value)); + Command.ExecuteNonQuery(); + Command.Parameters.Clear(); + + Command.CommandText = "SELECT t FROM TimeSpanDeclaredTime;"; + Command.ExecuteScalar().Should().Be(TimeOnly.FromTimeSpan(value)); + } + + [Fact] + public void SelectTimeSpanDeclaredTime() + { + Command.CommandText = "SELECT ?::TIME::VARCHAR, ? UNION ALL SELECT '', TIME '01:00';"; + Command.Parameters.Add(new DuckDBParameter(DbType.Time, new TimeSpan(0, 1, 30, 0))); + Command.Parameters.Add(new DuckDBParameter(DbType.Time, new TimeSpan(0, 2, 0, 0))); + + using var reader = Command.ExecuteReader(); + reader.Read(); + + reader.GetString(0).Should().Be("01:30:00"); + reader.GetFieldValue(1).Should().Be(new TimeOnly(2, 0)); + } + + [Theory] + [InlineData(1, 0)] + [InlineData(0, -1)] + public void TimeSpanDeclaredTime_OutOfRange_Throws(int days, int seconds) + { + var value = new TimeSpan(days, 0, 0, seconds); + + Command.CommandText = "SELECT ?;"; + Command.Parameters.Add(new DuckDBParameter(DbType.Time, value)); + + Command.Invoking(c => c.ExecuteScalar()).Should().Throw(); + } } From 43e8e2572f3eaf3b892392da04181edc4fad5ab8 Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Wed, 14 Oct 2026 15:17:57 -0500 Subject: [PATCH 26/26] Support ADO.NET schema data types and metadata probing --- DuckDB.NET.Data/DuckDBCommand.cs | 61 +++++-- DuckDB.NET.Data/DuckDBConnection.cs | 39 ++++- DuckDB.NET.Data/DuckDBDataReader.cs | 3 +- DuckDB.NET.Data/DuckDBParameter.cs | 21 ++- DuckDB.NET.Data/DuckDBParameterCollection.cs | 15 +- DuckDB.NET.Data/DuckDBSchema.cs | 156 +++++++++++++++++- DuckDB.NET.Data/DuckDBTransaction.cs | 5 +- .../PreparedStatement/PreparedStatement.cs | 5 +- DuckDB.NET.Test/SchemaTests.cs | 42 ++++- 9 files changed, 305 insertions(+), 42 deletions(-) diff --git a/DuckDB.NET.Data/DuckDBCommand.cs b/DuckDB.NET.Data/DuckDBCommand.cs index 6d602614..498727e9 100644 --- a/DuckDB.NET.Data/DuckDBCommand.cs +++ b/DuckDB.NET.Data/DuckDBCommand.cs @@ -12,6 +12,7 @@ public class DuckDBCommand : DbCommand { private DuckDBConnection? connection; private readonly DuckDBParameterCollection parameters = new(); + private bool readerOpen; protected override DbTransaction? DbTransaction { get; set; } protected override DbParameterCollection DbParameterCollection => parameters; @@ -19,7 +20,7 @@ public class DuckDBCommand : DbCommand public new virtual DuckDBParameterCollection Parameters => parameters; public override int CommandTimeout { get; set; } - public override CommandType CommandType { get; set; } + public override CommandType CommandType { get; set; } = CommandType.Text; public override bool DesignTimeVisible { get; set; } public override UpdateRowSource UpdatedRowSource { get; set; } @@ -39,7 +40,8 @@ public override string CommandText get; set { - // TODO: We shouldn't be able to change the CommandText when the command is in execution (requires CommandState implementation) + if (readerOpen) + throw new InvalidOperationException("CommandText cannot be changed while a reader is open."); field = value ?? string.Empty; } } = string.Empty; @@ -47,7 +49,12 @@ public override string CommandText protected override DbConnection? DbConnection { get => connection; - set => connection = (DuckDBConnection?)value; + set + { + if (readerOpen) + throw new InvalidOperationException("Connection cannot be changed while a reader is open."); + connection = (DuckDBConnection?)value; + } } public DuckDBCommand() @@ -64,7 +71,11 @@ public DuckDBCommand(string commandText, DuckDBConnection connection) Connection = connection; } - public override void Cancel() => connection?.NativeConnection.Interrupt(); + public override void Cancel() + { + if (connection?.State == ConnectionState.Open) + connection.NativeConnection.Interrupt(); + } public override int ExecuteNonQuery() { @@ -73,15 +84,19 @@ public override int ExecuteNonQuery() var results = PreparedStatement.PreparedStatement.PrepareMultiple(connection!.NativeConnection, CommandText, parameters, UseStreamingMode); var count = 0; + var returnedRows = false; foreach (var result in results) { var current = result; - count += (int)NativeMethods.Query.DuckDBRowsChanged(ref current); + if (NativeMethods.Query.DuckDBResultReturnType(current) == DuckDBResultType.QueryResult) + returnedRows = true; + else + count += (int)NativeMethods.Query.DuckDBRowsChanged(ref current); result.Close(); } - return count; + return returnedRows ? -1 : count; } public override object? ExecuteScalar() @@ -105,12 +120,20 @@ public override int ExecuteNonQuery() protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { EnsureConnectionOpen(); + if (readerOpen) + throw new InvalidOperationException("A reader is already open for this command."); var results = PreparedStatement.PreparedStatement.PrepareMultiple(connection!.NativeConnection, CommandText, parameters, UseStreamingMode); - - var reader = new DuckDBDataReader(this, results, behavior); - - return reader; + readerOpen = true; + try + { + return new DuckDBDataReader(this, results, behavior); + } + catch + { + readerOpen = false; + throw; + } } ///

@@ -164,17 +187,31 @@ public async IAsyncEnumerable ExecuteArrowBatchesAsync([EnumeratorC } } - public override void Prepare() { } + public override void Prepare() + { + EnsureConnectionOpen(); + if (string.IsNullOrWhiteSpace(CommandText)) + throw new InvalidOperationException("CommandText is required."); + } protected override DbParameter CreateDbParameter() => new DuckDBParameter(); internal void CloseConnection() => Connection!.Close(); + internal void ReaderClosed() => readerOpen = false; + private void EnsureConnectionOpen([CallerMemberName] string operation = "") { if (Connection is null || Connection.State != ConnectionState.Open) { throw new InvalidOperationException($"{operation} requires an open connection"); } + if (string.IsNullOrWhiteSpace(CommandText)) + throw new InvalidOperationException("CommandText is required."); + var openConnection = connection!; + if (DbTransaction is not null && !ReferenceEquals(DbTransaction.Connection, openConnection)) + throw new InvalidOperationException("The command transaction belongs to a different connection."); + if (openConnection.Transaction is not null && !ReferenceEquals(DbTransaction, openConnection.Transaction)) + throw new InvalidOperationException("The command transaction does not match the active connection transaction."); } -} \ No newline at end of file +} diff --git a/DuckDB.NET.Data/DuckDBConnection.cs b/DuckDB.NET.Data/DuckDBConnection.cs index 1d350dcf..48041077 100644 --- a/DuckDB.NET.Data/DuckDBConnection.cs +++ b/DuckDB.NET.Data/DuckDBConnection.cs @@ -38,7 +38,31 @@ public DuckDBConnection(string connectionString) [AllowNull] [DefaultValue("")] - public override string ConnectionString { get; set; } + public override string ConnectionString + { + get; + set + { + if (connectionState != ConnectionState.Closed) + throw new InvalidOperationException("The connection string cannot be changed while the connection is open."); + + var normalized = value ?? string.Empty; + if (!string.IsNullOrWhiteSpace(normalized)) + { + try + { + _ = DuckDBConnectionStringBuilder.Parse(normalized); + } + catch (Exception ex) when (ex is InvalidOperationException or ArgumentException) + { + throw new ArgumentException("The connection string is invalid.", nameof(value), ex); + } + } + + field = normalized; + parsedConnection = null; + } + } public override string Database { @@ -49,7 +73,7 @@ public override string Database return ParsedConnection.DataSource; } - throw new InvalidOperationException("Connection string must be specified."); + return string.Empty; } } @@ -62,7 +86,7 @@ public override string DataSource return ParsedConnection!.DataSource; } - throw new InvalidOperationException("Connection string must be specified."); + return string.Empty; } } @@ -72,7 +96,9 @@ public override string DataSource public DuckDBNativeConnection NativeConnection => connectionReference?.NativeConnection ?? throw new InvalidOperationException("The DuckDBConnection must be open to access the native connection."); - public override string ServerVersion => NativeMethods.Startup.DuckDBLibraryVersion(); + public override string ServerVersion => connectionState == ConnectionState.Open + ? NativeMethods.Startup.DuckDBLibraryVersion() + : throw new InvalidOperationException("The connection must be open."); public override ConnectionState State => connectionState; @@ -85,7 +111,7 @@ public override void Close() { if (connectionState == ConnectionState.Closed) { - throw new InvalidOperationException("Connection is already closed."); + return; } if (connectionReference is not null) //Should always be the case @@ -142,8 +168,7 @@ protected override DbCommand CreateDbCommand() { return new DuckDBCommand { - Connection = this, - Transaction = Transaction + Connection = this }; } diff --git a/DuckDB.NET.Data/DuckDBDataReader.cs b/DuckDB.NET.Data/DuckDBDataReader.cs index 06536ef9..42b12093 100644 --- a/DuckDB.NET.Data/DuckDBDataReader.cs +++ b/DuckDB.NET.Data/DuckDBDataReader.cs @@ -419,6 +419,7 @@ public override void Close() // Finish the reader's own cleanup first, so an exception from closing the connection cannot skip it. closed = true; resultEnumerator.Dispose(); + command.ReaderClosed(); if (behavior == CommandBehavior.CloseConnection) { @@ -440,4 +441,4 @@ private static void ThrowNoRowRead() { throw new InvalidOperationException("No row has been read"); } -} \ No newline at end of file +} diff --git a/DuckDB.NET.Data/DuckDBParameter.cs b/DuckDB.NET.Data/DuckDBParameter.cs index 7ace2d50..8619e955 100644 --- a/DuckDB.NET.Data/DuckDBParameter.cs +++ b/DuckDB.NET.Data/DuckDBParameter.cs @@ -6,15 +6,21 @@ namespace DuckDB.NET.Data; public class DuckDBParameter : DbParameter { - private const DbType DefaultDbType = DbType.String; + private const DbType DefaultDbType = DbType.Object; private object? value; + private string parameterName = string.Empty; + private string sourceColumn = string.Empty; public override DbType DbType { get; set; } [AllowNull] [DefaultValue("")] - public override string ParameterName { get; set; } + public override string ParameterName + { + get => parameterName; + set => parameterName = value ?? string.Empty; + } public override object? Value { @@ -29,12 +35,17 @@ public override object? Value } } - public override ParameterDirection Direction { get; set; } + public override ParameterDirection Direction { get; set; } = ParameterDirection.Input; public override bool IsNullable { get; set; } [AllowNull] [DefaultValue("")] - public override string SourceColumn { get; set; } + public override string SourceColumn + { + get => sourceColumn; + set => sourceColumn = value ?? string.Empty; + } + public override DataRowVersion SourceVersion { get; set; } = DataRowVersion.Current; public override bool SourceColumnNullMapping { get; set; } public override int Size { get; set; } @@ -66,4 +77,4 @@ public DuckDBParameter(string name, DbType type, object? value) } public override void ResetDbType() => DbType = DefaultDbType; -} \ No newline at end of file +} diff --git a/DuckDB.NET.Data/DuckDBParameterCollection.cs b/DuckDB.NET.Data/DuckDBParameterCollection.cs index d68c9134..ae0e727e 100644 --- a/DuckDB.NET.Data/DuckDBParameterCollection.cs +++ b/DuckDB.NET.Data/DuckDBParameterCollection.cs @@ -23,7 +23,7 @@ public class DuckDBParameterCollection : DbParameterCollection public override int Add(object value) { - parameters.Add((DuckDBParameter)value); + parameters.Add(RequireParameter(value)); return parameters.Count - 1; } @@ -33,9 +33,9 @@ public override int Add(object value) public override int IndexOf(object value) => parameters.IndexOf((DuckDBParameter) value); - public override void Insert(int index, object value) => parameters.Insert(index, (DuckDBParameter) value); + public override void Insert(int index, object value) => parameters.Insert(index, RequireParameter(value)); - public override void Remove(object value) => parameters.Remove((DuckDBParameter) value); + public override void Remove(object value) => parameters.Remove(RequireParameter(value)); public int Add(DuckDBParameter value) { @@ -61,12 +61,12 @@ public override void RemoveAt(string parameterName) } protected override void SetParameter(int index, DbParameter value) - => parameters[index] = (DuckDBParameter)value; + => parameters[index] = RequireParameter(value); protected override void SetParameter(string parameterName, DbParameter value) { var index = IndexOfSafe(parameterName); - parameters[index] = (DuckDBParameter)value; + parameters[index] = RequireParameter(value); } public override int IndexOf(string parameterName) @@ -100,4 +100,7 @@ private int IndexOfSafe(string parameterName) throw new IndexOutOfRangeException($"Parameter '{parameterName}' not found"); return index; } -} \ No newline at end of file + + private static DuckDBParameter RequireParameter(object? value) => + value as DuckDBParameter ?? throw new ArgumentNullException(nameof(value)); +} diff --git a/DuckDB.NET.Data/DuckDBSchema.cs b/DuckDB.NET.Data/DuckDBSchema.cs index 63e0e98b..f0395106 100644 --- a/DuckDB.NET.Data/DuckDBSchema.cs +++ b/DuckDB.NET.Data/DuckDBSchema.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Text; +using DuckDB.NET.Native; namespace DuckDB.NET.Data; @@ -13,10 +14,14 @@ internal static class DuckDBSchema private static readonly string[] IndexesRestrictions = ["index_catalog", "index_schema", "table_name", "index_name"]; - public static DataTable GetSchema(DuckDBConnection connection, string collectionName, string?[]? restrictionValues) => - collectionName.ToUpperInvariant() switch + public static DataTable GetSchema(DuckDBConnection connection, string collectionName, string?[]? restrictionValues) + { + ValidateRestrictions(collectionName, restrictionValues); + return collectionName.ToUpperInvariant() switch { "METADATACOLLECTIONS" => GetMetaDataCollections(), + "DATASOURCEINFORMATION" => GetDataSourceInformation(connection), + "DATATYPES" => GetDataTypes(), "RESTRICTIONS" => GetRestrictions(), "RESERVEDWORDS" => GetReservedWords(connection), "TABLES" => GetTables(connection, restrictionValues), @@ -25,6 +30,17 @@ public static DataTable GetSchema(DuckDBConnection connection, string collection "INDEXES" => GetIndexes(connection, restrictionValues), _ => throw new ArgumentOutOfRangeException(nameof(collectionName), collectionName, "Invalid collection name.") }; + } + + private static void ValidateRestrictions(string collectionName, string?[]? restrictionValues) + { + if (restrictionValues is { Length: > 0 } + && collectionName.ToUpperInvariant() is "METADATACOLLECTIONS" or "DATASOURCEINFORMATION" or "DATATYPES" or "RESTRICTIONS" or "RESERVEDWORDS") + { + throw new ArgumentException("This collection does not support restrictions.", nameof(restrictionValues)); + } + + } private static DataTable GetMetaDataCollections() => new(DbMetaDataCollectionNames.MetaDataCollections) @@ -37,9 +53,11 @@ private static DataTable GetMetaDataCollections() => }, Rows = { + { DbMetaDataCollectionNames.DataSourceInformation, 0, 0 }, + { DbMetaDataCollectionNames.DataTypes, 0, 0 }, { DbMetaDataCollectionNames.MetaDataCollections, 0, 0 }, - { DbMetaDataCollectionNames.Restrictions, 0, 0 }, { DbMetaDataCollectionNames.ReservedWords, 0, 0 }, + { DbMetaDataCollectionNames.Restrictions, 0, 0 }, { DuckDbMetaDataCollectionNames.Tables, TableRestrictions.Length, 3 }, { DuckDbMetaDataCollectionNames.Columns, ColumnRestrictions.Length, 4 }, { DuckDbMetaDataCollectionNames.ForeignKeys, ForeignKeyRestrictions.Length, 3 }, @@ -47,6 +65,130 @@ private static DataTable GetMetaDataCollections() => } }; + private static DataTable GetDataSourceInformation(DuckDBConnection connection) => + new(DbMetaDataCollectionNames.DataSourceInformation) + { + Columns = + { + { DbMetaDataColumnNames.CompositeIdentifierSeparatorPattern, typeof(string) }, + { DbMetaDataColumnNames.DataSourceProductName, typeof(string) }, + { DbMetaDataColumnNames.DataSourceProductVersion, typeof(string) }, + { DbMetaDataColumnNames.DataSourceProductVersionNormalized, typeof(string) }, + { DbMetaDataColumnNames.GroupByBehavior, typeof(int) }, + { DbMetaDataColumnNames.IdentifierCase, typeof(int) }, + { DbMetaDataColumnNames.IdentifierPattern, typeof(string) }, + { DbMetaDataColumnNames.OrderByColumnsInSelect, typeof(bool) }, + { DbMetaDataColumnNames.ParameterMarkerFormat, typeof(string) }, + { DbMetaDataColumnNames.ParameterMarkerPattern, typeof(string) }, + { DbMetaDataColumnNames.ParameterNameMaxLength, typeof(int) }, + { DbMetaDataColumnNames.ParameterNamePattern, typeof(string) }, + { DbMetaDataColumnNames.QuotedIdentifierCase, typeof(int) }, + { DbMetaDataColumnNames.QuotedIdentifierPattern, typeof(string) }, + { DbMetaDataColumnNames.StatementSeparatorPattern, typeof(string) }, + { DbMetaDataColumnNames.StringLiteralPattern, typeof(string) }, + { DbMetaDataColumnNames.SupportedJoinOperators, typeof(int) } + }, + Rows = + { + { + @"\.", "DuckDB", connection.ServerVersion, NormalizeVersion(connection.ServerVersion), + (int)GroupByBehavior.MustContainAll, + (int)IdentifierCase.Insensitive, + @"^[\p{L}_][\p{L}\p{N}_$]*$", false, + "${0}", @"(\$[\p{L}_][\p{L}\p{N}_]*)", DBNull.Value, + @"^[\p{L}_][\p{L}\p{N}_$]*$", (int)IdentifierCase.Insensitive, + "^(([^\\\"]|\\\"\\\")*)$", ";", @"'(([^']|'')*)'", (int)(SupportedJoinOperators.Inner | SupportedJoinOperators.LeftOuter + | SupportedJoinOperators.RightOuter | SupportedJoinOperators.FullOuter) + } + } + }; + + private static DataTable GetDataTypes() + { + var table = new DataTable(DbMetaDataCollectionNames.DataTypes); + foreach (var column in new (string Name, Type Type)[] + { + (DbMetaDataColumnNames.TypeName, typeof(string)), (DbMetaDataColumnNames.ProviderDbType, typeof(int)), + (DbMetaDataColumnNames.DataType, typeof(string)), (DbMetaDataColumnNames.ColumnSize, typeof(long)), + (DbMetaDataColumnNames.CreateFormat, typeof(string)), (DbMetaDataColumnNames.CreateParameters, typeof(string)), + (DbMetaDataColumnNames.IsAutoIncrementable, typeof(bool)), (DbMetaDataColumnNames.IsBestMatch, typeof(bool)), + (DbMetaDataColumnNames.IsCaseSensitive, typeof(bool)), (DbMetaDataColumnNames.IsConcurrencyType, typeof(bool)), + (DbMetaDataColumnNames.IsFixedLength, typeof(bool)), (DbMetaDataColumnNames.IsFixedPrecisionScale, typeof(bool)), + (DbMetaDataColumnNames.IsLiteralSupported, typeof(bool)), (DbMetaDataColumnNames.IsLong, typeof(bool)), + (DbMetaDataColumnNames.IsNullable, typeof(bool)), (DbMetaDataColumnNames.IsSearchable, typeof(bool)), + (DbMetaDataColumnNames.IsSearchableWithLike, typeof(bool)), (DbMetaDataColumnNames.IsUnsigned, typeof(bool)), + (DbMetaDataColumnNames.LiteralPrefix, typeof(string)), (DbMetaDataColumnNames.LiteralSuffix, typeof(string)), + (DbMetaDataColumnNames.MaximumScale, typeof(short)), (DbMetaDataColumnNames.MinimumScale, typeof(short)), + ("NativeDataType", typeof(string)) + }) + { + table.Columns.Add(column.Name, column.Type); + } + + AddType(table, "BOOLEAN", DuckDBType.Boolean, typeof(bool), isBestMatch: true); + AddType(table, "TINYINT", DuckDBType.TinyInt, typeof(sbyte), isBestMatch: true); + AddType(table, "SMALLINT", DuckDBType.SmallInt, typeof(short), isBestMatch: true); + AddType(table, "INTEGER", DuckDBType.Integer, typeof(int), isBestMatch: true); + AddType(table, "BIGINT", DuckDBType.BigInt, typeof(long), isBestMatch: true); + AddType(table, "UTINYINT", DuckDBType.UnsignedTinyInt, typeof(byte), isUnsigned: true, isBestMatch: true); + AddType(table, "USMALLINT", DuckDBType.UnsignedSmallInt, typeof(ushort), isUnsigned: true, isBestMatch: true); + AddType(table, "UINTEGER", DuckDBType.UnsignedInteger, typeof(uint), isUnsigned: true, isBestMatch: true); + AddType(table, "UBIGINT", DuckDBType.UnsignedBigInt, typeof(ulong), isUnsigned: true, isBestMatch: true); + AddType(table, "FLOAT", DuckDBType.Float, typeof(float), isBestMatch: true); + AddType(table, "DOUBLE", DuckDBType.Double, typeof(double), isBestMatch: true); + AddType(table, "DECIMAL", DuckDBType.Decimal, typeof(decimal), createFormat: "DECIMAL({0},{1})", createParameters: "precision,scale", isBestMatch: true); + AddType(table, "VARCHAR", DuckDBType.Varchar, typeof(string), long.MaxValue, isCaseSensitive: true, isBestMatch: true); + AddType(table, "BLOB", DuckDBType.Blob, typeof(byte[]), long.MaxValue, isLong: true, isBestMatch: true); + AddType(table, "UUID", DuckDBType.Uuid, typeof(Guid), isBestMatch: true); + AddType(table, "DATE", DuckDBType.Date, typeof(DateOnly), isBestMatch: true); + AddType(table, "TIME", DuckDBType.Time, typeof(TimeOnly), isBestMatch: true); + AddType(table, "TIMETZ", DuckDBType.TimeTz, typeof(DateTimeOffset), isBestMatch: true); + AddType(table, "TIMESTAMP", DuckDBType.Timestamp, typeof(DateTime), isBestMatch: true); + AddType(table, "TIMESTAMPTZ", DuckDBType.TimestampTz, typeof(DateTimeOffset), isBestMatch: true); + AddType(table, "INTERVAL", DuckDBType.Interval, typeof(TimeSpan), isBestMatch: true); + AddType(table, "HUGEINT", DuckDBType.HugeInt, typeof(System.Numerics.BigInteger), isBestMatch: true); + AddType(table, "UHUGEINT", DuckDBType.UnsignedHugeInt, typeof(System.Numerics.BigInteger), isUnsigned: true); + AddType(table, "VARINT", DuckDBType.VarInt, typeof(System.Numerics.BigInteger)); + AddType(table, "LIST", DuckDBType.List, typeof(object)); + AddType(table, "ARRAY", DuckDBType.Array, typeof(object)); + AddType(table, "STRUCT", DuckDBType.Struct, typeof(object)); + AddType(table, "MAP", DuckDBType.Map, typeof(object)); + AddType(table, "UNION", DuckDBType.Union, typeof(object)); + AddType(table, "ENUM", DuckDBType.Enum, typeof(string)); + AddType(table, "BIT", DuckDBType.Bit, typeof(string)); + + return table; + } + + private static void AddType( + DataTable table, + string typeName, + DuckDBType providerType, + Type dataType, + long columnSize = -1, + string? createFormat = null, + string? createParameters = null, + bool isUnsigned = false, + bool isCaseSensitive = false, + bool isLong = false, + bool isBestMatch = false) + { + table.Rows.Add( + typeName, (int)providerType, dataType.FullName!, columnSize, + createFormat ?? (object)DBNull.Value, createParameters ?? (object)DBNull.Value, + false, isBestMatch, isCaseSensitive, false, false, false, true, isLong, + true, true, isCaseSensitive, isUnsigned ? (object)true : DBNull.Value, + DBNull.Value, DBNull.Value, DBNull.Value, DBNull.Value, typeName); + } + + private static string NormalizeVersion(string serverVersion) + { + var value = serverVersion.TrimStart('v', 'V'); + return Version.TryParse(value, out var version) + ? $"{version.Major:00}.{version.Minor:000}.{(version.Build < 0 ? 0 : version.Build):0000}.{(version.Revision < 0 ? 0 : version.Revision):0000}" + : serverVersion; + } + private static DataTable GetRestrictions() => new(DbMetaDataCollectionNames.Restrictions) { @@ -74,10 +216,10 @@ private static DataTable GetRestrictions() => { DuckDbMetaDataCollectionNames.ForeignKeys, "Table", "table_name", 3 }, { DuckDbMetaDataCollectionNames.ForeignKeys, "Constraint", "constraint_name", 4 }, - { DuckDbMetaDataCollectionNames.Indexes, "Catalog", "constraint_catalog", 1 }, - { DuckDbMetaDataCollectionNames.Indexes, "Schema", "constraint_schema", 2 }, + { DuckDbMetaDataCollectionNames.Indexes, "Catalog", "index_catalog", 1 }, + { DuckDbMetaDataCollectionNames.Indexes, "Schema", "index_schema", 2 }, { DuckDbMetaDataCollectionNames.Indexes, "Table", "table_name", 3 }, - { DuckDbMetaDataCollectionNames.Indexes, "Constraint", "constraint_name", 4 }, + { DuckDbMetaDataCollectionNames.Indexes, "Index", "index_name", 4 }, }, }; @@ -218,4 +360,4 @@ private static DataTable GetDataTable(string tableName, DuckDBCommand command) return table; } -} \ No newline at end of file +} diff --git a/DuckDB.NET.Data/DuckDBTransaction.cs b/DuckDB.NET.Data/DuckDBTransaction.cs index 94a95cd9..aab103c9 100644 --- a/DuckDB.NET.Data/DuckDBTransaction.cs +++ b/DuckDB.NET.Data/DuckDBTransaction.cs @@ -35,7 +35,10 @@ private void FinishTransaction(string finalizer) try { - connection.ExecuteNonQuery(finalizer); + using var command = connection.CreateCommand(); + command.Transaction = this; + command.CommandText = finalizer; + command.ExecuteNonQuery(); Cleanup(); } // If something goes wrong with the transaction, to match the diff --git a/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs b/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs index 682dcb1e..54f977ce 100644 --- a/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs +++ b/DuckDB.NET.Data/PreparedStatement/PreparedStatement.cs @@ -98,7 +98,8 @@ private static void BindParameters(DuckDBPreparedStatement preparedStatement, Du for (var i = 0; i < count; i++) { var param = parameterCollection[i]; - var state = NativeMethods.PreparedStatements.DuckDBBindParameterIndex(preparedStatement, out var index, param.ParameterName); + var state = NativeMethods.PreparedStatements.DuckDBBindParameterIndex( + preparedStatement, out var index, param.ParameterName.TrimStart('$')); if (state.IsSuccess()) { BindParameter(preparedStatement, index, param); @@ -135,4 +136,4 @@ public void Dispose() { statement.Dispose(); } -} \ No newline at end of file +} diff --git a/DuckDB.NET.Test/SchemaTests.cs b/DuckDB.NET.Test/SchemaTests.cs index f9c2a4f4..e34885dd 100644 --- a/DuckDB.NET.Test/SchemaTests.cs +++ b/DuckDB.NET.Test/SchemaTests.cs @@ -20,6 +20,17 @@ public void InvalidCollection() { Assert.Throws(() => Connection.GetSchema("Invalid")); } + + [Theory] + [InlineData("MetaDataCollections")] + [InlineData("DataSourceInformation")] + [InlineData("DataTypes")] + [InlineData("Restrictions")] + [InlineData("ReservedWords")] + public void CollectionsWithoutRestrictionsRejectRestrictionValues(string collectionName) + { + Assert.Throws(() => Connection.GetSchema(collectionName, ["unexpected"])); + } [Fact] public void MetaDataCollections() @@ -119,6 +130,35 @@ public void ColumnsWithRestrictions() Assert.Equal("foo_id", schema.Rows[0]["column_name"]); } + [Fact] + public void ColumnsExposeDataType() + { + var schema = Connection.GetSchema("Columns", [null, null, "foo", "foo_id"]); + Assert.Equal("INTEGER", schema.Rows[0]["data_type"]); + } + + [Fact] + public void DataSourceInformationSupportsProbing() + { + var schema = Connection.GetSchema(DbMetaDataCollectionNames.DataSourceInformation); + var row = Assert.IsType(Assert.Single(schema.Rows)); + Assert.Equal("DuckDB", row["DataSourceProductName"]); + Assert.Equal(Connection.ServerVersion, row["DataSourceProductVersion"]); + Assert.Equal("${0}", row["ParameterMarkerFormat"]); + } + + [Fact] + public void DataTypesExposeStandardAdoNetMetadata() + { + var schema = Connection.GetSchema("DataTypes"); + var integer = schema.Rows.Cast().Single(row => (string)row["TypeName"] == "INTEGER"); + + Assert.Equal(typeof(int).FullName, integer["DataType"]); + Assert.Equal((int)DuckDBType.Integer, integer["ProviderDbType"]); + Assert.True((bool)integer["IsBestMatch"]); + Assert.True((bool)integer["IsSearchable"]); + } + [Fact] public void ColumnsTooManyRestrictions() { @@ -173,4 +213,4 @@ public void IndexesTooManyRestrictions() private static IEnumerable GetValues(DataTable schema, string columnName) => schema.Rows.Cast().Select(r => (string)r[columnName]); -} \ No newline at end of file +}