2323#include " duckdb/common/arrow/physical_arrow_collector.hpp"
2424#include " duckdb_python/arrow/arrow_export_utils.hpp"
2525
26+ namespace {
27+
28+ // A helper for arrow conversion. We want to be able to fetch a result's schema in the same transaction that
29+ // creates the result, so we have to wrap both calls in the same transaction. This helper always reverts the
30+ // transaction if we haven't committed it explicitly. Note that this is not the same as RunFunctionInTransaction:
31+ // we run _queries_ in a transaction (where each query acquires the context lock) while RFIT runs a function
32+ // while holding the context lock for that duration.
33+ // Note: this is a workaround that is intended to be temporary. We should really just cache the schema in the
34+ // ArrowQueryResult.
35+
36+ void RunOrThrow (duckdb::ClientContext &context, const char *sql) {
37+ auto result = context.Query (sql, duckdb::QueryParameters (false ));
38+ if (result->HasError ()) {
39+ result->ThrowError ();
40+ }
41+ }
42+
43+ class ArrowConversionTransaction {
44+ public:
45+ explicit ArrowConversionTransaction (duckdb::ClientContext &context_p) : context(context_p), owns(false ) {
46+ auto &txn = context.transaction ;
47+ if (txn.IsAutoCommit () && !txn.HasActiveTransaction ()) {
48+ RunOrThrow (context, " BEGIN TRANSACTION" );
49+ owns = true ;
50+ }
51+ }
52+
53+ ~ArrowConversionTransaction () {
54+ if (owns) {
55+ try {
56+ RunOrThrow (context, " ROLLBACK" );
57+ } catch (...) { // NOLINT
58+ }
59+ }
60+ }
61+
62+ void Commit () {
63+ if (owns) {
64+ RunOrThrow (context, " COMMIT" );
65+ owns = false ;
66+ }
67+ }
68+
69+ ArrowConversionTransaction (const ArrowConversionTransaction &) = delete ;
70+ ArrowConversionTransaction &operator =(const ArrowConversionTransaction &) = delete ;
71+
72+ private:
73+ duckdb::ClientContext &context;
74+ bool owns;
75+ };
76+
77+ } // namespace
78+
2679namespace duckdb {
2780
2881DuckDBPyRelation::DuckDBPyRelation (shared_ptr<Relation> rel_p) : rel(std::move(rel_p)) {
@@ -961,10 +1014,22 @@ PandasDataFrame DuckDBPyRelation::FetchDFChunk(idx_t vectors_per_chunk, bool dat
9611014}
9621015
9631016duckdb::pyarrow::Table DuckDBPyRelation::ToArrowTableInternal (idx_t batch_size, bool to_polars) {
1017+ if (!result && !rel) {
1018+ return py::none ();
1019+ }
1020+ // Make sure we have a valid client context
1021+ shared_ptr<ClientContext> context;
1022+ if (rel) {
1023+ context = rel->context ->GetContext ();
1024+ } else if (auto cc = result->GetClientProperties ().client_context ) {
1025+ context = cc->shared_from_this ();
1026+ } else {
1027+ throw ConnectionException (" Cannot fetch an arrow table without a valid connection" );
1028+ }
1029+ // Start (or piggyback on) a transaction for the conversion
1030+ ArrowConversionTransaction conversion_txn (*context);
1031+
9641032 if (!result) {
965- if (!rel) {
966- return py::none ();
967- }
9681033 auto &config = ClientConfig::GetConfig (*rel->context ->GetContext ());
9691034 ScopedConfigSetting scoped_setting (
9701035 config,
@@ -979,6 +1044,8 @@ duckdb::pyarrow::Table DuckDBPyRelation::ToArrowTableInternal(idx_t batch_size,
9791044 AssertResultOpen ();
9801045 auto res = result->FetchArrowTable (batch_size, to_polars);
9811046 result = nullptr ;
1047+ // We must commit the transaction before returning
1048+ conversion_txn.Commit ();
9821049 return res;
9831050}
9841051
0 commit comments