Skip to content

Allow Close to be called more than once - #373

Merged
Giorgi merged 1 commit into
Giorgi:developfrom
pengdows:fix/close-more-than-once
Oct 20, 2026
Merged

Giorgi merged 1 commit into
Giorgi:developfrom
pengdows:fix/close-more-than-once

Conversation

@alaricd

@alaricd alaricd commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

A second DuckDBConnection.Close() (or CloseAsync(), or Close() after Dispose()) throws InvalidOperationException: Connection is already closed.

The contract

DbConnection.Close, Remarks:

An application can call Close or CloseAsync more than one time. No exception is generated.

The same sentence is on SqlConnection.Close, and Npgsql, MySqlConnector, Microsoft.Data.Sqlite and SqlClient all accept a repeated Close.

Why it matters

The common pattern of closing explicitly and then letting using dispose works today only because Dispose checks the state first. Code that closes in more than one place (a finally plus a helper, or a data layer that closes on error and again on cleanup) gets an exception from DuckDB.NET and no other provider.

Background

The current behaviour was chosen deliberately in c0ea769 ("calling Close() on a connection when the connection is already closed or disposed will throw an exception"), with ExceptionOnDoubleClose and ExceptionOnDisposeThenClose pinning it. This PR replaces those two tests, because the documented contract says the opposite. If you would rather keep the exception, I'm happy to close this. The quote above is the whole argument.

Change

Close() returns when the connection is already closed. It does not raise StateChange a second time. Tests: CloseCanBeCalledMoreThanOnce (sync and async, and checks that StateChange fires once) and CloseAfterDisposeDoesNotThrow replace the two tests above. The full suite passes.

Conformance test: Close_can_be_called_more_than_once.

Found by the pengdows ADO.NET conformance suite, which runs the documented System.Data.Common contract against every provider; each failure it reports cites the learn.microsoft.com sentence the provider departs from.

🤖 Generated with Claude Code

DbConnection.Close: "An application can call Close or CloseAsync more
than one time. No exception is generated." A second Close returns
without raising StateChange again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 39415913370

Coverage increased (+0.06%) to 92.552%

Details

  • Coverage increased (+0.06%) from the base build.
  • Patch coverage: 1 of 1 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 4012
Covered Lines: 3776
Line Coverage: 94.12%
Relevant Branches: 2285
Covered Branches: 2052
Branch Coverage: 89.8%
Branches in Coverage %: Yes
Coverage Strength: 1003949.62 hits per line

💛 - Coveralls

@Giorgi

Giorgi commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Thanks, this looks good to me.

Could you add one more test: Close() on a connection that was never opened. With this change it returns early as well, and nothing pins that yet. I checked what Npgsql 10.0.0 does against PostgreSQL 18.3: Close(), Close(); Close() and CloseAsync(); CloseAsync() on a connection that was never opened all return without an exception, the state stays Closed, and StateChange is not raised. Something like:

[Fact]
public async Task CloseNeverOpenedConnectionDoesNotThrow()
{
    await using var duckDBConnection = new DuckDBConnection("DataSource=:memory:");
    var stateChanges = 0;
    duckDBConnection.StateChange += (_, _) => stateChanges++;

    duckDBConnection.Close();
    await duckDBConnection.CloseAsync();

    duckDBConnection.State.Should().Be(ConnectionState.Closed);
    stateChanges.Should().Be(0, "a connection that was never opened does not change state");
}

@Giorgi
Giorgi merged commit a16b0af into Giorgi:develop Oct 20, 2026
11 of 12 checks passed

This branch is waiting to be deployed

1 waiting deployment
external — 2f413e71 Waiting Oct 20, 2026 by alaricd via Authorize #677
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants