Repository: duckdb/duckdb
File: test/appender/test_appender.cpp (C++)

Code before:
}

TEST_CASE("Interrupted QueryAppender flow: interrupt -> clear -> close finishes", "[appender]") {
	DuckDB db(nullptr);
	Connection con(db);

	REQUIRE_NO_FAIL(con.Query("CREATE TABLE ints(i INTEGER)"));

	// Prepare a long time running QueryAppender
	duckdb::vector<LogicalType> types = {LogicalType::INTEGER};
	duckdb::vector<string> names = {"i"};
	// This query will run for a long time by cross joining a huge range
	string long_query = "INSERT INTO ints SELECT i FROM appended_data, range(1000000000000)";
	QueryAppender app(con, long_query, types, names);

	// Append a single row so we actually have something to flush
	app.AppendRow(1);;

	atomic<bool> flush_started{false};

	thread t([&]() {
		flush_started.store(true);
		try {
			app.Flush();
		} catch (...) {
			// Expected when the query is interrupted
		}
	});

	// Wait until the flush thread starts, then interrupt
	while (!flush_started.load()) {
		this_thread::yield();
	}
	// Give the flush a tiny moment to get into execution before interrupting
	std::this_thread::sleep_for(std::chrono::milliseconds(50));
	con.Interrupt();

	t.join();

	// Now clear pending buffers so Close will not attempt to flush again
	app.Clear();

	// Should finish eventually. Close must complete quickly since no data remains to flush
	auto future = std::async(std::launch::async, [&]() {
	    app.Close();
	});

	auto status = future.wait_for(std::chrono::seconds(1));

	if (status == std::future_status::ready) {
	    REQUIRE_NOTHROW(future.get());

Review comment (suggestion):
Maybe we can catch the exception and ensure it is the one we expect? 
```cpp
catch (std::exception &ex) {
			ErrorData error_data(ex);
// Get the exception type and ensure its `INTERRUPT`
		}
```

```cpp
	//! Get the internal exception type of the error.
	DUCKDB_API const ExceptionType &Type() const;
```

Code after:

	// We're clearing, which means we're expecting to have only the `1` appended after the clear.
	appender.Clear();
	appender.AppendRow(1);
	appender.Close();

	duckdb::unique_ptr<QueryResult> result = con.Query("SELECT * FROM ints");
	REQUIRE(CHECK_COLUMN(result, 0, {1}));
}

TEST_CASE("Interrupted QueryAppender flow: interrupt -> clear -> close finishes", "[appender]") {
	DuckDB db(nullptr);
	Connection con(db);

	REQUIRE_NO_FAIL(con.Query("CREATE TABLE ints(i INTEGER)"));

	// Prepare a long time running QueryAppender
	duckdb::vector<LogicalType> types = {LogicalType::INTEGER};
	duckdb::vector<string> names = {"i"};
	// This query will run for a long time by cross joining a huge range
	string long_query = "INSERT INTO ints SELECT i FROM appended_data, range(1000000000000)";
	QueryAppender app(con, long_query, types, names);

	// Append a single row so we actually have something to flush
	app.AppendRow(1);

	atomic<bool> flush_started {false};

	thread t([&]() {
		flush_started.store(true);
		try {
			app.Flush();
		} catch (std::exception &ex) {
			ErrorData error_data(ex);
			REQUIRE((error_data.Type() == ExceptionType::INTERRUPT));
		}
	});

	// Wait until the flush thread starts, then interrupt
	while (!flush_started.load()) {
		this_thread::yield();
	}
	// Give the flush a tiny moment to get into execution before interrupting
	std::this_thread::sleep_for(std::chrono::milliseconds(50));
	con.Interrupt();

	t.join();

	// Now clear pending buffers so Close will not attempt to flush again
	app.Clear();