Skip to content

Commit ed8ba44

Browse files
authored
Fix Arrow validity buffer lifetime in PyQuery (#2294)
1 parent 5139981 commit ed8ba44

2 files changed

Lines changed: 39 additions & 5 deletions

File tree

tiledb/core.cc

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,6 +1524,14 @@ class PyQuery {
15241524
for (auto& buffer_name : buffers_order_) {
15251525
BufferInfo& buffer_info = buffers_.at(buffer_name);
15261526

1527+
// Convert validity to bitmap BEFORE creating BufferHolder
1528+
int64_t null_count = 0;
1529+
if (is_nullable(buffer_name)) {
1530+
null_count = count_zeros(buffer_info.validity);
1531+
buffer_info.validity = uint8_bool_to_uint8_bitmap(
1532+
buffer_info.validity);
1533+
}
1534+
15271535
auto buffer_holder = new BufferHolder(
15281536
buffer_info.data, buffer_info.validity, buffer_info.offsets);
15291537

@@ -1538,11 +1546,7 @@ class PyQuery {
15381546
buffer_holder);
15391547

15401548
if (is_nullable(buffer_name)) {
1541-
// count zeros before converting to bitmap
1542-
c_pa_array.null_count = count_zeros(buffer_info.validity);
1543-
// convert to bitmap
1544-
buffer_info.validity = uint8_bool_to_uint8_bitmap(
1545-
buffer_info.validity);
1549+
c_pa_array.null_count = null_count;
15461550
c_pa_array.buffers[0] = buffer_info.validity.data();
15471551
c_pa_array.n_buffers = is_var(buffer_name) ? 3 : 2;
15481552
c_pa_schema.flags |= ARROW_FLAG_NULLABLE;

tiledb/tests/test_core.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import random
33

44
import numpy as np
5+
import pytest
56
from numpy.testing import assert_array_equal
67

78
import tiledb
@@ -157,3 +158,32 @@ def test_import_buffer(self):
157158
self.assertTrue("foo" in r)
158159
self.assertTrue("str" not in r)
159160
del q
161+
162+
def test_nullable_arrow_buffer(self):
163+
# BufferHolder must hold reference to converted bitmap, not original.
164+
# Corrupted validity buffer causes wrong null positions in .to_pandas().
165+
pytest.importorskip("pandas")
166+
pyarrow = pytest.importorskip("pyarrow")
167+
168+
def _read_arrow(uri):
169+
with tiledb.open(uri, "r") as A:
170+
q = core.PyQuery(A.ctx, A, ("a",), (), 0, True)
171+
sub = tiledb.Subarray(A)
172+
sub.add_dim_range(0, (0, 4))
173+
q.set_subarray(sub)
174+
q.submit()
175+
return q._buffers_to_pa_table()
176+
177+
uri = self.path("test_nullable_arrow_buffer")
178+
dom = tiledb.Domain(tiledb.Dim("d", domain=(0, 4), tile=1, dtype=np.uint64))
179+
attr = tiledb.Attr("a", dtype="ascii", var=True, nullable=True)
180+
tiledb.Array.create(
181+
uri, tiledb.ArraySchema(domain=dom, attrs=[attr], sparse=True)
182+
)
183+
184+
with tiledb.open(uri, "w") as A:
185+
A[np.arange(5)] = {"a": pyarrow.array(["x", "y", None, None, ""])}
186+
187+
df = _read_arrow(uri).to_pandas()
188+
189+
assert df["a"].isna().tolist() == [False, False, True, True, False]

0 commit comments

Comments
 (0)