Repository navigation
Description
Describe the bug, including details regarding any error messages, version, and platform.
I'm trying to import/export data to database in python through ArrayStream over pyarrow.jvm and JDBC.
In order to export ArrowVectorIterator as stream without unloading to RecordBatch on java side before it export to stream, I wrap ArrowVectorIterator into ArrowReader as below:
public class ArrowVectorIteratorReader extends ArrowReader {
private final Iterator<VectorSchemaRoot> iterator;
private final Schema schema;
private VectorSchemaRoot root;
public ArrowVectorIteratorReader(BufferAllocator allocator, Iterator<VectorSchemaRoot> iterator, Schema schema) {
super(allocator);
this.iterator = iterator;
this.schema = schema;
this.root = null;
}
@Override
public VectorSchemaRoot getVectorSchemaRoot() throws IOException {
if (root == null) return super.getVectorSchemaRoot();
return root;
}
@Override
public boolean loadNextBatch() throws IOException {
if (iterator.hasNext()) {
VectorSchemaRoot lastRoot = root;
root = iterator.next();
if (root != lastRoot && lastRoot != null) lastRoot.close();
return true;
} else {
return false;
}
}
@Override
public long bytesRead() {
return 0;
}
@Override
protected void closeReadSource() throws IOException {
if (iterator instanceof AutoCloseable) {
try {
((AutoCloseable) iterator).close();
} catch (Exception e) {
throw new IOException(e);
}
}
root.close();
}
@Override
protected Schema readSchema() throws IOException {
return schema;
}
}When ArrowVectorIterator use the config with reuseVectorSchemaRoot is enabled, utf8 array may crushed on python side, but works as expectred on java side.
Java code as below
try (final ArrowReader source = porter.importData(1); returns ArrowVectorIteratorReader with batchSize=1
final ArrowArrayStream stream = ArrowArrayStream.allocateNew(allocator)) {
Data.exportArrayStream(allocator, source, stream);
try (final ArrowReader reader = Data.importArrayStream(allocator, stream)) {
while (reader.loadNextBatch()) {
// root from getVectorSchemaRoot() is legal on every vector
totalRecord += reader.getVectorSchemaRoot().getRowCount();
}
}
}On Python side, the situation is unexplainable.
The exported stream from Java in wrapped into a RecordBatchReader and write into different file formats.
def wrap_from_java_stream_to_generator(java_arrow_stream, allocator=None, yield_schema=False):
if allocator is None:
allocator = get_java_root_allocator().allocator
c_stream = arrow_c.new("struct ArrowArrayStream*")
c_stream_ptr = int(arrow_c.cast("uintptr_t", c_stream))
org = jpype. JPackage("org")
java_wrapped_stream = org.apache.arrow.c.ArrowArrayStream.wrap(c_stream_ptr)
org.apache.arrow.c.Data.exportArrayStream(allocator, java_arrow_stream, java_wrapped_stream)
# noinspection PyProtectedMember
with pa. RecordBatchReader._import_from_c(c_stream_ptr) as reader: # type: pa. RecordBatchReader
if yield_schema:
yield reader.schema
yield from reader
def wrap_from_java_stream(java_arrow_stream, allocator=None):
generator = wrap_from_java_stream_to_generator(java_arrow_stream, allocator, yield_schema=True)
schema = next(generator)
return pa. RecordBatchReader.from_batches(schema, generator)For CSV, works as expected
with wrap_from_java_stream(java_arrow_stream, allocator) as stream:
with pa.csv.CSVWriter(csv_path, stream.schema) as writer:
for record_batch in stream:
writer.write_batch(record_batch)For Parquet, writing with dataset api as below
with wrap_from_java_stream(java_arrow_stream, allocator) as stream:
pa.dataset.write_dataset(stream, data_path, format="parquet")_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
......./python3.8/site-packages/pyarrow/dataset.py:999: in write_dataset
_filesystemdataset_write(
pyarrow/_dataset.pyx:3655: in pyarrow._dataset._filesystemdataset_write
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
> ???
E pyarrow.lib.ArrowInvalid: Parquet cannot store strings with size 2GB or more
OR
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
> ???
E pyarrow.lib.ArrowInvalid: Column 1: In chunk 0: Invalid: Length spanned by binary offsets (7) larger than values array (size 6)
In order to making out which record raises error, RecordBatchReader is wrapped into a smaller batch size and log the content as below:
with wrap_from_java_stream(java_arrow_stream, allocator) as stream:
def generator():
for rb in stream:
for i in range(rb.num_rows):
slice = rb.slice(i,1)
logger.info(slice.to_pylist())
yield slice
pa.dataset.write_dataset(pa.RecordBatchReader.from_batches(stream.schema, generator(), data_path, format="parquet")Although the logger can print the slice, but write_dataset fails
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
......./python3.8/site-packages/pyarrow/dataset.py:999: in write_dataset
_filesystemdataset_write(
pyarrow/_dataset.pyx:3655: in pyarrow._dataset._filesystemdataset_write
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
> ???
E pyarrow.lib.ArrowInvalid: Column 1: In chunk 0: Invalid: First or last binary offset out of bounds
For arrow/feather format, it seems directly write record_batch into files, but when record_batch is invalid when reading from file (code is similar as above)
Then, if I create the ArrowVectorIteratorReader without reuseVectorSchemaRoot, everything works fine on Python side.
Component(s)
Java, Python
Activity
lidavidm commentedon Jul 4, 2023
Is it possible to share a self-contained reproduction?
That said, I think what might be happening is that the Parquet writer may request more than one batch from the reader, and if you request to share roots, then the previous batch will be overwritten. That is, I would expect this to fail:
Exporting data via C Data does not copy the data, so it is your application's responsibility to properly manage the lifetime of the buffers. And Arrow Java uses mutable buffers, so if you enable reusing a VectorSchemaRoot, you'll find that reading new data invalidates previously read data.
lidavidm commentedon Jul 4, 2023
(you may have to read more batches than that to get things to fail, but I hope the idea is clear)
hu6360567 commentedon Jul 4, 2023
That explains the failure in my code. Since Arrow Java prefers use the same root to populate data, and so as it in default
ArrowReaderimplementation, the base class to export to C Data. Like "InMemoeryArrowReader", it should fail in such scenario.Do we have any document to explain why java implementation perfers the same root, also why C Data Stream is bound to
ArrowReader, rather thanIterator<VectorSchemaRoot>.The ArrowReader default implementation always need to convert to RecordBatch once if only
loadNextBatchoverrided.lidavidm commentedon Jul 4, 2023
There just isn't a good interface for this in Java. Arrow Java was designed differently from the C++ implementation.
lidavidm commentedon Jul 4, 2023
I suppose the C Data implementation should unload from the root when it exports.
[-][Java][Python] export ArrowVectorIterator to python fails randomly, if reuseVectorSchemaRoot enabled[/-][+][Java][Python] Avoid Reuse VectorSchemaRoot for exporting ArrowArrayStream to other language[/+]lidavidm commentedon Jul 7, 2023
We should fix this behavior in Java (and ensure it's tested) since it is a surprise.
lidavidm commentedon Jul 7, 2023
CC @davisusanibar
davisusanibar commentedon Aug 22, 2023
Hi @hu6360567 sorry to join late,
Could you help me to validate if this is working on your side please?
I'm able to create parquet file with data obtained from the database using JDBC adapter and then use C Data Interface to read that from python side:
Testing:
Java Produces of Arrow Reader:
Python Side:
Java POM
DML/DDL
davisusanibar commentedon Aug 22, 2023
Alternatively, you might want to consider https://github.com/davisusanibar/java-python-by-cdata
lidavidm commentedon Aug 22, 2023
@davisusanibar the 'right' fix is probably to unload the root when exporting and to make exports work in terms of ArrowRecordBatch instead of VectorSchemaRoot
davisusanibar commentedon Aug 22, 2023
Yes, that is my second step, first I want to validate that it is working, then I can create a new issue for that enhancement.
davisusanibar commentedon Aug 22, 2023
I have a doubt about how-to close resources properly, all of them are open, because any of this are inside try-with-resources.
lidavidm commentedon Aug 22, 2023
It would generally look like
The only problem is the lifetime of the
BufferAllocatorwhich isn't solvable in C Data Interface; either the Java application needs to have a singleton allocator or (preferably) it should tie the lifetime of the allocator to something else (e.g. an init/finalize call, or if the Java application implements a reader or some other object, it should tie the allocator to that object)hu6360567 commentedon Aug 22, 2023
It cannot use a very close lifetime scoped
BufferAllocator, since the python gc is not very reliable. We are using a sapaterate allocator for export streams that outlives themselves.When record_batch at python side is garbage collected, it will make a callback to the owner to release. But it not always as expected, since there is no RAII like object to live as long as a
withstatement, you may check earlier discussion in mailing list.https://lists.apache.org/thread/glog4g0gkm1c7lz7nx6opso7d97sot49
davisusanibar commentedon Sep 5, 2023
Just created this PR for cookbook, I would appreciate it if you could help me validate the notes mentions
apache/arrow-cookbook#325
suibianwanwank commentedon Oct 12, 2024
Hi, I’m new to Arrow, and I’ve encountered a similar issue where I need to convert an ArrowVectorIterator to an ArrowArrayStream. Do you have any good suggestions on how to approach this? Thanks for your advice.