jbonofre commented on code in PR #1419:
URL: https://github.com/apache/arrow-java/pull/1419#discussion_r4451002117
##########
c/src/main/java/org/apache/arrow/c/ArrayImporter.java:
##########
@@ -84,15 +100,78 @@ private void importChild(ArrayImporter parent, ArrowArray
src) {
}
private void doImport(ArrowArray.Snapshot snapshot) {
- // A non-zero offset is only meaningful for arrays that have buffers to
offset into.
- // Null arrays carry no buffers, so their offset is inert and safe to
ignore.
- checkState(
- snapshot.offset == 0
- || snapshot.length == 0
- || vector.getField().getType().getTypeID() ==
ArrowType.ArrowTypeID.Null,
- "ArrowArray struct has non-zero offset (%s), which is not supported",
- snapshot.offset);
+ checkState(snapshot.offset >= 0, "ArrowArray struct has negative offset
(%s)", snapshot.offset);
+ if (snapshot.offset != 0 && snapshot.length != 0) {
+ importSliced(snapshot);
+ } else {
+ importUnsliced(snapshot, /* skippedRows= */ 0, /* trimChildren= */
false);
+ }
+ }
+
+ /**
+ * Java vectors cannot represent an offset, so the rows up to the end of the
slice are imported
+ * into a temporary vector and the slice is then transferred into the
target. Leading rows that
+ * fill whole bytes of a bitmap are skipped by advancing the buffer
pointers, so that the
+ * temporary vector, and any validity bitmap synthesized for it, has fewer
than 8 rows before the
+ * slice.
+ */
+ private void importSliced(ArrowArray.Snapshot snapshot) {
+ // Run-end encoded arrays have no buffers, and their children are not
indexed by row
+ long skippedRows =
+ storageType().getTypeID() == ArrowTypeID.RunEndEncoded ? 0 :
snapshot.offset & ~7L;
+ int offset = checkedCastToInt(snapshot.offset - skippedRows);
+ int length = checkedCastToInt(snapshot.length);
+
+ snapshot.offset = 0;
+ snapshot.length = Math.addExact(offset, length);
+ snapshot.null_count = unknownNullCount(snapshot);
+
+ Field unslicedField = field != null ? field : vector.getField();
Review Comment:
When `field` is null (the public `Data.importIntoVector(allocator, array,
vector, provider)`), the temporary vector is built from `vector.getField()`.
For a dense union, that doesn't reproduce the target.
`DenseUnionVector.getField()` walks `typeFields`, which is indexed by type id,
so it returns the children sorted by type id. The C array and the target keep
the producer's order.
With type ids `[10, 5]` (children `a`, `b`), the temporary vector has
children `[b, a]`. `importUnsliced` loads C child 0 (`a`) into the temporary
type-5 vector, and `createTransferPairs()` then rewrites the target's
`typeMapFields` in the temporary vector's order. If both children are
`IntVector`, `checkTransferable` passes and rows read values from the wrong
child, with no error. If the classes differ, it throws a misleading "its field
create a ..." error.
The same array imports correctly with offset `0`, and through
`Data.importVector`, which passes the field from the C schema. So only the
sliced import without a field is affected. All the dense union tests go through
`importVector`, and `testImportDenseUnionWithTypeIdsWithNonZeroOffset` uses
ascending ids with children of different types, so none of them catch this.
```java
Field field = new Field("u",
FieldType.nullable(new ArrowType.Union(UnionMode.Dense, new int[] {10,
5})),
Arrays.asList(
Field.nullable("a", MinorType.INT.getType()),
Field.nullable("b", MinorType.INT.getType())));
// source rows: a=100, b=201, a=102, b=203
Data.exportVector(allocator, source, null, array);
slice(array, 1, 3);
try (FieldVector target = field.createVector(allocator)) {
Data.importIntoVector(allocator, array, target, null);
// expected [201, 102, 203]
}
```
Could we build the temporary field in the target's child order (children
from `getChildrenFromFields()`, type ids from `typeMapFields`), or make
`DenseUnionVector.getField()` keep the child order?
##########
c/src/main/java/org/apache/arrow/c/ArrayImporter.java:
##########
@@ -84,15 +100,78 @@ private void importChild(ArrayImporter parent, ArrowArray
src) {
}
private void doImport(ArrowArray.Snapshot snapshot) {
- // A non-zero offset is only meaningful for arrays that have buffers to
offset into.
- // Null arrays carry no buffers, so their offset is inert and safe to
ignore.
- checkState(
- snapshot.offset == 0
- || snapshot.length == 0
- || vector.getField().getType().getTypeID() ==
ArrowType.ArrowTypeID.Null,
- "ArrowArray struct has non-zero offset (%s), which is not supported",
- snapshot.offset);
+ checkState(snapshot.offset >= 0, "ArrowArray struct has negative offset
(%s)", snapshot.offset);
+ if (snapshot.offset != 0 && snapshot.length != 0) {
+ importSliced(snapshot);
+ } else {
+ importUnsliced(snapshot, /* skippedRows= */ 0, /* trimChildren= */
false);
+ }
+ }
+
+ /**
+ * Java vectors cannot represent an offset, so the rows up to the end of the
slice are imported
+ * into a temporary vector and the slice is then transferred into the
target. Leading rows that
+ * fill whole bytes of a bitmap are skipped by advancing the buffer
pointers, so that the
+ * temporary vector, and any validity bitmap synthesized for it, has fewer
than 8 rows before the
+ * slice.
+ */
+ private void importSliced(ArrowArray.Snapshot snapshot) {
+ // Run-end encoded arrays have no buffers, and their children are not
indexed by row
+ long skippedRows =
+ storageType().getTypeID() == ArrowTypeID.RunEndEncoded ? 0 :
snapshot.offset & ~7L;
+ int offset = checkedCastToInt(snapshot.offset - skippedRows);
+ int length = checkedCastToInt(snapshot.length);
+
+ snapshot.offset = 0;
+ snapshot.length = Math.addExact(offset, length);
+ snapshot.null_count = unknownNullCount(snapshot);
+
+ Field unslicedField = field != null ? field : vector.getField();
+ try (FieldVector unslicedVector = unslicedField.createVector(allocator)) {
Review Comment:
`Field.createVector` builds a struct with the default `CONFLICT_REPLACE`
policy, so duplicate child names collapse into one.
`importIntoVectorSchemaRoot` uses `StructVector.emptyWithDuplicates` on purpose
for this case (see `testVectorSchemaRootWithDuplicatedFieldNames`), but the
sliced path loses that. With columns `[a, a, b]` and a non-zero offset, the
temporary struct has 2 children, and `importUnsliced` fails with `ArrowArray
struct has 3 children (expected 2)`. The same array imports fine with offset 0.
Keeping the duplicates in the temporary vector is not enough on its own.
`StructTransferPair` maps children by name (`from.getChild(child)`), so both a
columns would be routed to the first one.
One option is to create the temporary struct with the target's conflict
policy and transfer struct children by position. Another is to skip the
temporary vector for structs: the importer already pushes the offset into
struct children, so only the struct's own validity bitmap would need shifting.
##########
vector/src/main/codegen/templates/DenseUnionVector.java:
##########
@@ -669,29 +672,54 @@ public void splitAndTransfer(int startIndex, int length) {
to.reallocOffsetBuffer();
}
- int [] typeCounts = new int[nextTypeId];
- int [] typeStarts = new int[nextTypeId];
- for (int i = 0; i < typeCounts.length; i++) {
- typeCounts[i] = 0;
- typeStarts[i] = -1;
+ int[] childIndexes = new int[typeFields.length];
+ Arrays.fill(childIndexes, -1);
+ for (int i = 0; i < nextTypeId; i++) {
+ childIndexes[typeMapFields[i]] = i;
}
+ // The window selects childCounts[i] values of child i, which are
renumbered from 0. The
+ // offsets of a child only have to increase, so the selected values may
have gaps.
+ int[] childCounts = new int[nextTypeId];
+ int[] childStarts = new int[nextTypeId];
+ int[] childEnds = new int[nextTypeId];
+ boolean[] contiguous = new boolean[nextTypeId];
+ Arrays.fill(contiguous, true);
for (int i = startIndex; i < startIndex + length; i++) {
byte typeId = typeBuffer.getByte(i);
if (typeId >= 0) {
- to.offsetBuffer.setInt((long) (i - startIndex) * OFFSET_WIDTH,
typeCounts[typeId]);
- typeCounts[typeId] += 1;
- if (typeStarts[typeId] == -1) {
- typeStarts[typeId] = offsetBuffer.getInt((long) i * OFFSET_WIDTH);
+ int child = childIndexes[typeId];
+ int offset = offsetBuffer.getInt((long) i * OFFSET_WIDTH);
+ if (childCounts[child] == 0) {
+ childStarts[child] = offset;
+ } else if (offset != childEnds[child]) {
+ contiguous[child] = false;
}
+ childEnds[child] = offset + 1;
+ to.offsetBuffer.setInt((long) (i - startIndex) * OFFSET_WIDTH,
childCounts[child]);
+ childCounts[child] += 1;
}
}
- // transfer vector values
+ // transfer vector values, and copy those of children whose selected
values have gaps
+ for (int i = 0; i < nextTypeId; i++) {
+ if (childCounts[i] > 0 && contiguous[i]) {
+ internalTransferPairs[i].splitAndTransfer(childStarts[i],
childCounts[i]);
+ to.childVectors[typeMapFields[i]] = internalTransferPairs[i].getTo();
+ }
+ }
+ for (int i = startIndex; i < startIndex + length; i++) {
+ byte typeId = typeBuffer.getByte(i);
+ if (typeId >= 0 && !contiguous[childIndexes[typeId]]) {
+ internalTransferPairs[childIndexes[typeId]].copyValueSafe(
Review Comment:
The fallback for children whose offsets have gaps copies values with
`copyValueSafe`, which not every child type supports:
- `RunEndEncodedVector` doesn't override `copyFrom`, so it hits
`BaseValueVector.copyFrom` and throws an `UnsupportedOperationException` with
no message.
- `LargeListVector.copyFrom` throws "LargeListViewVector does not support
copyFrom operation yet".
So `splitAndTransfer` fails for a dense union with a run-end encoded or
large list view child whose offsets in the window are not consecutive, and so
does a sliced C import of one. The exception is thrown after the contiguous
children were already transferred, so the target is left partly filled.
`setValueCount()` recomputes the child value counts from the type buffer, so
I agree the target can't keep the gaps and copying is the way to go.
Could we check up front that every child that needs copying supports it, and
otherwise throw an error that names the child type?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]