Skip to content

Commit 7ddc75f

Browse files
committed
run schema fetching in same transaction as arrow data conversion and only once
1 parent 5fd7f69 commit 7ddc75f

7 files changed

Lines changed: 103 additions & 29 deletions

File tree

src/duckdb_py/arrow/arrow_array_stream.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
namespace duckdb {
1616

17-
void TransformDuckToArrowChunk(ArrowSchema &arrow_schema, ArrowArray &data, py::list &batches) {
17+
void TransformDuckToArrowChunk(py::object pyarrow_schema, ArrowArray &data, py::list &batches) {
1818
py::gil_assert();
1919
auto pyarrow_lib_module = py::module::import("pyarrow").attr("lib");
2020
auto batch_import_func = pyarrow_lib_module.attr("RecordBatch").attr("_import_from_c");
21-
batches.append(batch_import_func(reinterpret_cast<uint64_t>(&data), reinterpret_cast<uint64_t>(&arrow_schema)));
21+
batches.append(batch_import_func(reinterpret_cast<uint64_t>(&data), pyarrow_schema));
2222
}
2323

2424
void VerifyArrowDatasetLoaded() {

src/duckdb_py/arrow/arrow_export_utils.cpp

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,28 @@ namespace duckdb {
1717

1818
namespace pyarrow {
1919

20-
py::object ToArrowTable(const vector<LogicalType> &types, const vector<string> &names, const py::list &batches,
21-
ClientProperties &options) {
20+
py::object ToPyArrowSchema(ArrowSchema &schema) {
2221
py::gil_scoped_acquire acquire;
2322

2423
auto pyarrow_lib_module = py::module::import("pyarrow").attr("lib");
25-
auto from_batches_func = pyarrow_lib_module.attr("Table").attr("from_batches");
2624
auto schema_import_func = pyarrow_lib_module.attr("Schema").attr("_import_from_c");
25+
return schema_import_func(reinterpret_cast<uint64_t>(&schema));
26+
}
27+
28+
py::object ToArrowTable(const py::list &batches, py::object pyarrow_schema) {
29+
py::gil_scoped_acquire acquire;
30+
31+
auto pyarrow_lib_module = py::module::import("pyarrow").attr("lib");
32+
auto from_batches_func = pyarrow_lib_module.attr("Table").attr("from_batches");
33+
34+
return py::cast<duckdb::pyarrow::Table>(from_batches_func(batches, pyarrow_schema));
35+
}
36+
37+
py::object ToArrowTable(const vector<LogicalType> &types, const vector<string> &names, const py::list &batches,
38+
ClientProperties &options) {
2739
ArrowSchema schema;
2840
ArrowConverter::ToArrowSchema(&schema, types, names, options);
29-
auto schema_obj = schema_import_func(reinterpret_cast<uint64_t>(&schema));
30-
31-
return py::cast<duckdb::pyarrow::Table>(from_batches_func(batches, schema_obj));
41+
return ToArrowTable(batches, ToPyArrowSchema(schema));
3242
}
3343

3444
} // namespace pyarrow

src/duckdb_py/include/duckdb_python/arrow/arrow_array_stream.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ enum class PyArrowObjectType {
6262
PolarsLazyFrame
6363
};
6464

65-
void TransformDuckToArrowChunk(ArrowSchema &arrow_schema, ArrowArray &data, py::list &batches);
65+
void TransformDuckToArrowChunk(py::object pyarrow_schema, ArrowArray &data, py::list &batches);
6666

6767
PyArrowObjectType GetArrowType(const py::handle &obj);
6868

src/duckdb_py/include/duckdb_python/arrow/arrow_export_utils.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ namespace duckdb {
66

77
namespace pyarrow {
88

9+
py::object ToPyArrowSchema(ArrowSchema &schema);
10+
911
py::object ToArrowTable(const vector<LogicalType> &types, const vector<string> &names, const py::list &batches,
1012
ClientProperties &options);
1113

14+
py::object ToArrowTable(const py::list &batches, py::object pyarrow_schema);
15+
1216
} // namespace pyarrow
1317

1418
} // namespace duckdb

src/duckdb_py/pyrelation.cpp

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,59 @@
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+
2679
namespace duckdb {
2780

2881
DuckDBPyRelation::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

9631016
duckdb::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

src/duckdb_py/pyresult.cpp

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -526,21 +526,19 @@ duckdb::pyarrow::Table DuckDBPyResult::FetchArrowTable(idx_t rows_per_batch, boo
526526
QueryResult::DeduplicateColumns(names);
527527
}
528528

529+
// Fetch the schema once
530+
ArrowSchema arrow_schema;
531+
ArrowConverter::ToArrowSchema(&arrow_schema, result->types, names, result->client_properties);
532+
auto pyarrow_schema = pyarrow::ToPyArrowSchema(arrow_schema);
533+
529534
py::list batches;
530535
if (result->type == QueryResultType::ARROW_RESULT) {
531536
auto &arrow_result = result->Cast<ArrowQueryResult>();
532537
auto arrays = arrow_result.ConsumeArrays();
533538
for (auto &array : arrays) {
534-
ArrowSchema arrow_schema;
535-
auto result_names = arrow_result.names;
536-
if (to_polars) {
537-
QueryResult::DeduplicateColumns(result_names);
538-
}
539539
ArrowArray data = array->arrow_array;
540540
array->arrow_array.release = nullptr;
541-
ArrowConverter::ToArrowSchema(&arrow_schema, arrow_result.types, result_names,
542-
arrow_result.client_properties);
543-
TransformDuckToArrowChunk(arrow_schema, data, batches);
541+
TransformDuckToArrowChunk(pyarrow_schema, data, batches);
544542
}
545543
} else {
546544
// STREAM_RESULT: pull the live stream directly into Arrow batches.
@@ -558,17 +556,11 @@ duckdb::pyarrow::Table DuckDBPyResult::FetchArrowTable(idx_t rows_per_batch, boo
558556
if (count == 0) {
559557
break;
560558
}
561-
ArrowSchema arrow_schema;
562-
auto result_names = result->names;
563-
if (to_polars) {
564-
QueryResult::DeduplicateColumns(result_names);
565-
}
566-
ArrowConverter::ToArrowSchema(&arrow_schema, result->types, result_names, result->client_properties);
567-
TransformDuckToArrowChunk(arrow_schema, data, batches);
559+
TransformDuckToArrowChunk(pyarrow_schema, data, batches);
568560
}
569561
}
570562

571-
return pyarrow::ToArrowTable(result->types, names, std::move(batches), result->client_properties);
563+
return pyarrow::ToArrowTable(std::move(batches), pyarrow_schema);
572564
}
573565

574566
ArrowArrayStream DuckDBPyResult::FetchArrowArrayStream(idx_t rows_per_batch) {

src/duckdb_py/python_udf.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ static py::list ConvertToSingleBatch(vector<LogicalType> &types, vector<string>
2525
ClientProperties &options, ClientContext &context) {
2626
ArrowSchema schema;
2727
ArrowConverter::ToArrowSchema(&schema, types, names, options);
28+
auto pyarrow_schema = pyarrow::ToPyArrowSchema(schema);
2829

2930
py::list single_batch;
3031
ArrowAppender appender(types, STANDARD_VECTOR_SIZE, options,
3132
ArrowTypeExtensionData::GetExtensionTypes(context, types));
3233
appender.Append(input, 0, input.size(), input.size());
3334
auto array = appender.Finalize();
34-
TransformDuckToArrowChunk(schema, array, single_batch);
35+
TransformDuckToArrowChunk(pyarrow_schema, array, single_batch);
3536
return single_batch;
3637
}
3738

0 commit comments

Comments
 (0)