getMap(Batch batch, String name, int index) {
+ Column column = batch.getColumn(name);
+ if (column == null || column.isNull(index)) {
+ return null;
+ }
+ if (!(column instanceof MapColumn)) {
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + name
+ + "' has unsupported map column type '" + column.getClass().getSimpleName() + "'.");
+ }
+ return ((MapColumn) column).get(index);
+ }
+
+ //endregion
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReader.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReader.java
new file mode 100644
index 000000000000..c1fb68b7c5ec
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReader.java
@@ -0,0 +1,554 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util;
+
+import com.azure.storage.blob.implementation.models.BlobListArrowParseException;
+import com.azure.storage.blob.implementation.util.arrow.Buffer;
+import com.azure.storage.blob.implementation.util.arrow.Endianness;
+import com.azure.storage.blob.implementation.util.arrow.Field;
+import com.azure.storage.blob.implementation.util.arrow.FieldNode;
+import com.azure.storage.blob.implementation.util.arrow.Int;
+import com.azure.storage.blob.implementation.util.arrow.KeyValue;
+import com.azure.storage.blob.implementation.util.arrow.Message;
+import com.azure.storage.blob.implementation.util.arrow.MessageHeader;
+import com.azure.storage.blob.implementation.util.arrow.RecordBatch;
+import com.azure.storage.blob.implementation.util.arrow.Schema;
+import com.azure.storage.blob.implementation.util.arrow.TimeUnit;
+import com.azure.storage.blob.implementation.util.arrow.Timestamp;
+import com.azure.storage.blob.implementation.util.arrow.Type;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Minimal Apache Arrow IPC stream reader scoped to the needs of the ListBlobs Arrow response.
+ *
+ * This reader intentionally supports only the subset of the Arrow IPC format emitted by the Storage ListBlobs
+ * endpoint: a single schema message followed by zero or more uncompressed, little-endian record batches whose
+ * columns are UTF-8 strings, booleans, integers, second-precision timestamps, and map<string,string>. Anything
+ * outside that subset (dictionaries, compression, big-endian, unsupported types) fails fast with
+ * {@link BlobListArrowParseException}.
+ *
+ * It depends only on the {@code arrow-format} flatbuffer definitions for metadata decoding and reads record batch
+ * bodies directly, so it does not require the {@code arrow-vector} runtime.
+ */
+final class BlobListArrowStreamReader {
+
+ private static final int CONTINUATION_MARKER = 0xFFFFFFFF;
+
+ private BlobListArrowStreamReader() {
+ }
+
+ /**
+ * The decoded contents of an Arrow IPC stream.
+ */
+ static final class Parsed {
+ private final Map schemaMetadata;
+ private final List batches;
+
+ Parsed(Map schemaMetadata, List batches) {
+ this.schemaMetadata = schemaMetadata;
+ this.batches = batches;
+ }
+
+ Map getSchemaMetadata() {
+ return schemaMetadata;
+ }
+
+ List getBatches() {
+ return batches;
+ }
+ }
+
+ /**
+ * A single decoded record batch: a row count and columns addressable by field name.
+ */
+ static final class Batch {
+ private final int rowCount;
+ private final Map columns;
+
+ Batch(int rowCount, Map columns) {
+ this.rowCount = rowCount;
+ this.columns = columns;
+ }
+
+ int getRowCount() {
+ return rowCount;
+ }
+
+ Column getColumn(String name) {
+ return columns.get(name);
+ }
+ }
+
+ /**
+ * Reads and decodes an Arrow IPC stream.
+ *
+ * @param stream the Arrow IPC stream.
+ * @return the decoded schema metadata and record batches.
+ * @throws BlobListArrowParseException if the stream is malformed or uses an unsupported feature.
+ */
+ static Parsed read(InputStream stream) {
+ byte[] bytes;
+ try {
+ bytes = readAll(stream);
+ } catch (IOException e) {
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: unable to read IPC stream.", e);
+ }
+
+ ByteBuffer body = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+
+ Map schemaMetadata = null;
+ List fields = null;
+ List batches = new ArrayList<>();
+
+ int pos = 0;
+ int length = bytes.length;
+ while (pos + 4 <= length) {
+ int marker = body.getInt(pos);
+ pos += 4;
+
+ int metadataLength;
+ if (marker == CONTINUATION_MARKER) {
+ if (pos + 4 > length) {
+ break;
+ }
+ metadataLength = body.getInt(pos);
+ pos += 4;
+ } else {
+ // Pre-0.15 streams used a bare length prefix without the continuation marker.
+ metadataLength = marker;
+ }
+
+ if (metadataLength == 0) {
+ // End-of-stream marker.
+ break;
+ }
+ if (metadataLength < 0 || pos + metadataLength > length) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: message metadata length is out of bounds.");
+ }
+
+ ByteBuffer messageBuffer
+ = ByteBuffer.wrap(bytes, pos, metadataLength).slice().order(ByteOrder.LITTLE_ENDIAN);
+ Message message = Message.getRootAsMessage(messageBuffer);
+ pos += metadataLength;
+
+ long bodyLength = message.bodyLength();
+ if (bodyLength < 0 || pos + bodyLength > length) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: message body length is out of bounds.");
+ }
+ int bodyStart = pos;
+ pos += (int) bodyLength;
+
+ byte headerType = message.headerType();
+ if (headerType == MessageHeader.SCHEMA) {
+ Schema schema = (Schema) message.header(new Schema());
+ if (schema == null) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: schema message header is missing.");
+ }
+ if (schema.endianness() != Endianness.LITTLE) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: only little-endian streams are supported.");
+ }
+ schemaMetadata = readKeyValueMetadata(schema);
+ fields = readFields(schema);
+ } else if (headerType == MessageHeader.RECORD_BATCH) {
+ if (fields == null) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: record batch encountered before schema.");
+ }
+ RecordBatch recordBatch = (RecordBatch) message.header(new RecordBatch());
+ if (recordBatch == null) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: record batch message header is missing.");
+ }
+ if (recordBatch.compression() != null) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: compressed record batches are not supported.");
+ }
+ batches.add(buildBatch(fields, recordBatch, body, bodyStart));
+ } else if (headerType == MessageHeader.DICTIONARY_BATCH) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: dictionary-encoded streams are not supported.");
+ }
+ // Other header types (Tensor, SparseTensor) are not expected and are ignored.
+ }
+
+ if (fields == null) {
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: stream contained no schema.");
+ }
+
+ return new Parsed(schemaMetadata == null ? new HashMap<>() : schemaMetadata, batches);
+ }
+
+ private static Batch buildBatch(List fields, RecordBatch recordBatch, ByteBuffer body, int bodyStart) {
+ BatchCursor cursor = new BatchCursor(recordBatch, body, bodyStart);
+ Map columns = new LinkedHashMap<>();
+ for (ArrowField field : fields) {
+ columns.put(field.name, buildColumn(field, cursor));
+ }
+ return new Batch((int) recordBatch.length(), columns);
+ }
+
+ private static Column buildColumn(ArrowField field, BatchCursor cursor) {
+ FieldNode node = cursor.nextNode();
+ int valueCount = (int) node.length();
+
+ switch (field.typeType) {
+ case Type.UTF8:
+ case Type.BINARY: {
+ BufferRegion validity = cursor.nextBuffer();
+ BufferRegion offsets = cursor.nextBuffer();
+ BufferRegion data = cursor.nextBuffer();
+ return new StringColumn(valueCount, validity, offsets, data, cursor.body, cursor.bodyStart);
+ }
+
+ case Type.BOOL: {
+ BufferRegion validity = cursor.nextBuffer();
+ BufferRegion data = cursor.nextBuffer();
+ return new BoolColumn(valueCount, validity, data, cursor.body, cursor.bodyStart);
+ }
+
+ case Type.INT: {
+ BufferRegion validity = cursor.nextBuffer();
+ BufferRegion data = cursor.nextBuffer();
+ return new IntColumn(valueCount, validity, data, field.bitWidth, field.signed, cursor.body,
+ cursor.bodyStart);
+ }
+
+ case Type.TIMESTAMP: {
+ BufferRegion validity = cursor.nextBuffer();
+ BufferRegion data = cursor.nextBuffer();
+ return new TimestampColumn(valueCount, validity, data, cursor.body, cursor.bodyStart);
+ }
+
+ case Type.MAP: {
+ BufferRegion validity = cursor.nextBuffer();
+ BufferRegion offsets = cursor.nextBuffer();
+ // Map has a single Struct child ("entries") with key and value children.
+ ArrowField entries = field.children.get(0);
+ StructColumn struct = (StructColumn) buildColumn(entries, cursor);
+ Column keyColumn = struct.children.get(0);
+ Column valueColumn = struct.children.get(1);
+ if (!(keyColumn instanceof StringColumn) || !(valueColumn instanceof StringColumn)) {
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + field.name
+ + "' map entries must be string keys and values.");
+ }
+ return new MapColumn(valueCount, validity, offsets, (StringColumn) keyColumn,
+ (StringColumn) valueColumn, cursor.body, cursor.bodyStart);
+ }
+
+ case Type.STRUCT: {
+ cursor.nextBuffer(); // struct validity buffer
+ List children = new ArrayList<>(field.children.size());
+ for (ArrowField child : field.children) {
+ children.add(buildColumn(child, cursor));
+ }
+ return new StructColumn(children);
+ }
+
+ default:
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + field.name
+ + "' has unsupported Arrow type '" + Type.name(field.typeType) + "'.");
+ }
+ }
+
+ private static List readFields(Schema schema) {
+ int count = schema.fieldsLength();
+ List fields = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ fields.add(readField(schema.fields(i)));
+ }
+ return fields;
+ }
+
+ private static ArrowField readField(Field field) {
+ ArrowField arrowField = new ArrowField();
+ arrowField.name = field.name();
+ arrowField.typeType = field.typeType();
+
+ if (arrowField.typeType == Type.INT) {
+ Int intType = (Int) field.type(new Int());
+ if (intType != null) {
+ arrowField.bitWidth = intType.bitWidth();
+ arrowField.signed = intType.isSigned();
+ }
+ } else if (arrowField.typeType == Type.TIMESTAMP) {
+ Timestamp timestamp = (Timestamp) field.type(new Timestamp());
+ if (timestamp != null && timestamp.unit() != TimeUnit.SECOND) {
+ throw new BlobListArrowParseException("ListBlobs Arrow parse failure: field '" + arrowField.name
+ + "' uses an unsupported timestamp unit '" + TimeUnit.name(timestamp.unit()) + "'.");
+ }
+ }
+
+ int childCount = field.childrenLength();
+ arrowField.children = new ArrayList<>(childCount);
+ for (int i = 0; i < childCount; i++) {
+ arrowField.children.add(readField(field.children(i)));
+ }
+ return arrowField;
+ }
+
+ private static Map readKeyValueMetadata(Schema schema) {
+ int count = schema.customMetadataLength();
+ if (count == 0) {
+ return new HashMap<>();
+ }
+ Map metadata = new HashMap<>();
+ for (int i = 0; i < count; i++) {
+ KeyValue keyValue = schema.customMetadata(i);
+ metadata.put(keyValue.key(), keyValue.value());
+ }
+ return metadata;
+ }
+
+ private static byte[] readAll(InputStream stream) throws IOException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ byte[] chunk = new byte[8192];
+ int read;
+ while ((read = stream.read(chunk)) != -1) {
+ buffer.write(chunk, 0, read);
+ }
+ return buffer.toByteArray();
+ }
+
+ /**
+ * A parsed schema field with the minimal type information required for decoding.
+ */
+ private static final class ArrowField {
+ private String name;
+ private byte typeType;
+ private int bitWidth;
+ private boolean signed;
+ private List children = new ArrayList<>();
+ }
+
+ /**
+ * Sequentially hands out the field nodes and buffers of a record batch in pre-order.
+ */
+ private static final class BatchCursor {
+ private final RecordBatch recordBatch;
+ private final ByteBuffer body;
+ private final int bodyStart;
+ private int nodeIndex;
+ private int bufferIndex;
+
+ BatchCursor(RecordBatch recordBatch, ByteBuffer body, int bodyStart) {
+ this.recordBatch = recordBatch;
+ this.body = body;
+ this.bodyStart = bodyStart;
+ }
+
+ FieldNode nextNode() {
+ if (nodeIndex >= recordBatch.nodesLength()) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: record batch is missing expected field nodes.");
+ }
+ return recordBatch.nodes(nodeIndex++);
+ }
+
+ BufferRegion nextBuffer() {
+ if (bufferIndex >= recordBatch.buffersLength()) {
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: record batch is missing expected buffers.");
+ }
+ Buffer buffer = recordBatch.buffers(bufferIndex++);
+ return new BufferRegion(buffer.offset(), buffer.length());
+ }
+ }
+
+ /**
+ * Offset and length of a single buffer within the record batch body.
+ */
+ private static final class BufferRegion {
+ private final long offset;
+ private final long length;
+
+ BufferRegion(long offset, long length) {
+ this.offset = offset;
+ this.length = length;
+ }
+ }
+
+ /**
+ * Base class for decoded columns.
+ */
+ abstract static class Column {
+ final int valueCount;
+ final BufferRegion validity;
+ final ByteBuffer body;
+ final int bodyStart;
+
+ Column(int valueCount, BufferRegion validity, ByteBuffer body, int bodyStart) {
+ this.valueCount = valueCount;
+ this.validity = validity;
+ this.body = body;
+ this.bodyStart = bodyStart;
+ }
+
+ boolean isNull(int index) {
+ if (validity == null || validity.length == 0) {
+ return false;
+ }
+ int bytePosition = bodyStart + (int) validity.offset + (index >> 3);
+ int bit = (body.get(bytePosition) >> (index & 7)) & 1;
+ return bit == 0;
+ }
+ }
+
+ /**
+ * UTF-8 string column (Arrow Utf8/Binary).
+ */
+ static final class StringColumn extends Column {
+ private final BufferRegion offsets;
+ private final BufferRegion data;
+
+ StringColumn(int valueCount, BufferRegion validity, BufferRegion offsets, BufferRegion data, ByteBuffer body,
+ int bodyStart) {
+ super(valueCount, validity, body, bodyStart);
+ this.offsets = offsets;
+ this.data = data;
+ }
+
+ String get(int index) {
+ int start = body.getInt(bodyStart + (int) offsets.offset + index * 4);
+ int end = body.getInt(bodyStart + (int) offsets.offset + (index + 1) * 4);
+ int dataStart = bodyStart + (int) data.offset + start;
+ byte[] valueBytes = new byte[end - start];
+ for (int i = 0; i < valueBytes.length; i++) {
+ valueBytes[i] = body.get(dataStart + i);
+ }
+ return new String(valueBytes, StandardCharsets.UTF_8);
+ }
+ }
+
+ /**
+ * Boolean column stored as a bitmap (Arrow Bool).
+ */
+ static final class BoolColumn extends Column {
+ private final BufferRegion data;
+
+ BoolColumn(int valueCount, BufferRegion validity, BufferRegion data, ByteBuffer body, int bodyStart) {
+ super(valueCount, validity, body, bodyStart);
+ this.data = data;
+ }
+
+ boolean get(int index) {
+ int bytePosition = bodyStart + (int) data.offset + (index >> 3);
+ int bit = (body.get(bytePosition) >> (index & 7)) & 1;
+ return bit == 1;
+ }
+ }
+
+ /**
+ * Integer column (Arrow Int) of width 8/16/32/64, signed or unsigned, returned as a long.
+ */
+ static final class IntColumn extends Column {
+ private final BufferRegion data;
+ private final int bitWidth;
+ private final boolean signed;
+
+ IntColumn(int valueCount, BufferRegion validity, BufferRegion data, int bitWidth, boolean signed,
+ ByteBuffer body, int bodyStart) {
+ super(valueCount, validity, body, bodyStart);
+ this.data = data;
+ this.bitWidth = bitWidth;
+ this.signed = signed;
+ }
+
+ long get(int index) {
+ int base = bodyStart + (int) data.offset;
+ switch (bitWidth) {
+ case 64:
+ return body.getLong(base + index * 8);
+
+ case 32: {
+ int value = body.getInt(base + index * 4);
+ return signed ? value : (value & 0xFFFFFFFFL);
+ }
+
+ case 16: {
+ short value = body.getShort(base + index * 2);
+ return signed ? value : (value & 0xFFFF);
+ }
+
+ case 8: {
+ byte value = body.get(base + index);
+ return signed ? value : (value & 0xFF);
+ }
+
+ default:
+ throw new BlobListArrowParseException(
+ "ListBlobs Arrow parse failure: unsupported integer bit width '" + bitWidth + "'.");
+ }
+ }
+ }
+
+ /**
+ * Second-precision timestamp column (Arrow Timestamp, SECOND unit).
+ */
+ static final class TimestampColumn extends Column {
+ private final BufferRegion data;
+
+ TimestampColumn(int valueCount, BufferRegion validity, BufferRegion data, ByteBuffer body, int bodyStart) {
+ super(valueCount, validity, body, bodyStart);
+ this.data = data;
+ }
+
+ long getEpochSeconds(int index) {
+ return body.getLong(bodyStart + (int) data.offset + index * 8);
+ }
+ }
+
+ /**
+ * Struct column holding ordered child columns (used internally for map entries).
+ */
+ static final class StructColumn extends Column {
+ private final List children;
+
+ StructColumn(List children) {
+ super(0, null, null, 0);
+ this.children = children;
+ }
+ }
+
+ /**
+ * Map<string,string> column (Arrow Map of Struct<key:utf8,value:utf8>).
+ */
+ static final class MapColumn extends Column {
+ private final BufferRegion offsets;
+ private final StringColumn keys;
+ private final StringColumn values;
+
+ MapColumn(int valueCount, BufferRegion validity, BufferRegion offsets, StringColumn keys, StringColumn values,
+ ByteBuffer body, int bodyStart) {
+ super(valueCount, validity, body, bodyStart);
+ this.offsets = offsets;
+ this.keys = keys;
+ this.values = values;
+ }
+
+ Map get(int index) {
+ int start = body.getInt(bodyStart + (int) offsets.offset + index * 4);
+ int end = body.getInt(bodyStart + (int) offsets.offset + (index + 1) * 4);
+ Map map = new HashMap<>();
+ for (int entry = start; entry < end; entry++) {
+ map.put(keys.get(entry), values.get(entry));
+ }
+ return map.isEmpty() ? null : map;
+ }
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java
index cb859dfa595d..6bd5deb8cc55 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/ModelHelper.java
@@ -47,6 +47,7 @@
import com.azure.storage.blob.models.PageBlobCopyIncrementalRequestConditions;
import com.azure.storage.blob.models.PageRange;
import com.azure.storage.blob.models.ParallelTransferOptions;
+import com.azure.storage.blob.models.StorageResponseSerializationFormat;
import com.azure.storage.blob.models.TaggedBlobItem;
import com.azure.storage.common.Utility;
import com.azure.storage.common.implementation.Constants;
@@ -85,6 +86,14 @@ public final class ModelHelper {
*/
public static final int PAGE_BYTES = 512;
+ /**
+ * The format that {@link StorageResponseSerializationFormat#AUTO} currently resolves to on the
+ * wire. Changing this constant is a behavioral change (and should be noted in the CHANGELOG)
+ * but it is not a public API change.
+ */
+ private static final StorageResponseSerializationFormat DEFAULT_SERIALIZATION_FORMAT
+ = StorageResponseSerializationFormat.XML;
+
/**
* Determines whether the passed authority is IP style, that is, it is of the format {@code :}.
*
@@ -663,6 +672,22 @@ public static BlobStorageException mapToBlobStorageException(BlobStorageExceptio
internal.getResponse(), code, headerName), internal.getResponse(), internal.getValue());
}
+ /**
+ * Resolves a user-supplied {@link StorageResponseSerializationFormat} to the concrete value
+ * to send on the wire. Treats {@code null} and {@link StorageResponseSerializationFormat#AUTO}
+ * identically — both yield {@link #DEFAULT_SERIALIZATION_FORMAT}.
+ *
+ * @param format the format requested by the caller, or {@code null} if unset.
+ * @return the concrete {@link StorageResponseSerializationFormat} to send on the wire.
+ */
+ public static StorageResponseSerializationFormat
+ resolveSerializationFormat(StorageResponseSerializationFormat format) {
+ if (format == null || format == StorageResponseSerializationFormat.AUTO) {
+ return DEFAULT_SERIALIZATION_FORMAT;
+ }
+ return format;
+ }
+
private ModelHelper() {
}
}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/BodyCompression.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/BodyCompression.java
new file mode 100644
index 000000000000..1def5fa220bd
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/BodyCompression.java
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code BodyCompression} table.
+ *
+ * The ListBlobs reader only needs to detect the presence of this table to reject compressed record batches, so no
+ * fields are exposed.
+ */
+public final class BodyCompression extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public BodyCompression __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Buffer.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Buffer.java
new file mode 100644
index 000000000000..2fc6c28447ce
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Buffer.java
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+import com.google.flatbuffers.Struct;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code Buffer} struct (offset/length of a buffer within a record batch body).
+ */
+public final class Buffer extends Struct {
+ /**
+ * Positions this accessor at the given struct offset.
+ *
+ * @param i the struct offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given struct offset.
+ *
+ * @param i the struct offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Buffer __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the byte offset of the buffer relative to the start of the record batch body.
+ *
+ * @return the buffer offset.
+ */
+ public long offset() {
+ return bb.getLong(bb_pos);
+ }
+
+ /**
+ * Gets the length, in bytes, of the buffer.
+ *
+ * @return the buffer length.
+ */
+ public long length() {
+ return bb.getLong(bb_pos + 8);
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Endianness.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Endianness.java
new file mode 100644
index 000000000000..91c30efe2edb
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Endianness.java
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+/**
+ * Values for the Arrow IPC {@code Endianness} enum.
+ */
+public final class Endianness {
+ private Endianness() {
+ }
+
+ /** Little-endian byte order. */
+ public static final short LITTLE = 0;
+ /** Big-endian byte order. */
+ public static final short BIG = 1;
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Field.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Field.java
new file mode 100644
index 000000000000..f121785d16e1
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Field.java
@@ -0,0 +1,99 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code Field} table describing a single column.
+ */
+public final class Field extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Field __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the field name.
+ *
+ * @return the field name, or {@code null} when absent.
+ */
+ public String name() {
+ int o = __offset(4);
+ return o != 0 ? __string(o + bb_pos) : null;
+ }
+
+ /**
+ * Gets the discriminator identifying the field's {@code type} union (see {@link Type}).
+ *
+ * @return the type union discriminator, or {@code 0} when absent.
+ */
+ public byte typeType() {
+ int o = __offset(8);
+ return o != 0 ? bb.get(o + bb_pos) : 0;
+ }
+
+ /**
+ * Resolves the {@code type} union value into the supplied accessor.
+ *
+ * @param obj the accessor to assign to the union value.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public Table type(Table obj) {
+ int o = __offset(10);
+ return o != 0 ? __union(obj, o + bb_pos) : null;
+ }
+
+ /**
+ * Gets the child field at the given index.
+ *
+ * @param j the child index.
+ * @return the child field accessor.
+ */
+ public Field children(int j) {
+ return children(new Field(), j);
+ }
+
+ /**
+ * Gets the child field at the given index into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @param j the child index.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public Field children(Field obj, int j) {
+ int o = __offset(14);
+ return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null;
+ }
+
+ /**
+ * Gets the number of child fields.
+ *
+ * @return the child field count.
+ */
+ public int childrenLength() {
+ int o = __offset(14);
+ return o != 0 ? __vector_len(o) : 0;
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/FieldNode.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/FieldNode.java
new file mode 100644
index 000000000000..bc44e0d0ccb6
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/FieldNode.java
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+import com.google.flatbuffers.Struct;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code FieldNode} struct (per-column metadata within a record batch).
+ */
+public final class FieldNode extends Struct {
+ /**
+ * Positions this accessor at the given struct offset.
+ *
+ * @param i the struct offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given struct offset.
+ *
+ * @param i the struct offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public FieldNode __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the number of value slots in the column.
+ *
+ * @return the value count.
+ */
+ public long length() {
+ return bb.getLong(bb_pos);
+ }
+
+ /**
+ * Gets the number of null value slots in the column.
+ *
+ * @return the null count.
+ */
+ public long nullCount() {
+ return bb.getLong(bb_pos + 8);
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Int.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Int.java
new file mode 100644
index 000000000000..efbddb99e3ff
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Int.java
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code Int} type table.
+ */
+public final class Int extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Int __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the bit width of the integer (8, 16, 32, or 64).
+ *
+ * @return the bit width, or {@code 0} when absent.
+ */
+ public int bitWidth() {
+ int o = __offset(4);
+ return o != 0 ? bb.getInt(o + bb_pos) : 0;
+ }
+
+ /**
+ * Gets whether the integer is signed.
+ *
+ * @return {@code true} if signed, otherwise {@code false}.
+ */
+ public boolean isSigned() {
+ int o = __offset(6);
+ return o != 0 && 0 != bb.get(o + bb_pos);
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/KeyValue.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/KeyValue.java
new file mode 100644
index 000000000000..c70a275a3278
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/KeyValue.java
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code KeyValue} table (a single custom metadata entry).
+ */
+public final class KeyValue extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public KeyValue __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the metadata key.
+ *
+ * @return the key, or {@code null} when absent.
+ */
+ public String key() {
+ int o = __offset(4);
+ return o != 0 ? __string(o + bb_pos) : null;
+ }
+
+ /**
+ * Gets the metadata value.
+ *
+ * @return the value, or {@code null} when absent.
+ */
+ public String value() {
+ int o = __offset(6);
+ return o != 0 ? __string(o + bb_pos) : null;
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Message.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Message.java
new file mode 100644
index 000000000000..f68b14f57281
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Message.java
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Accessor for the Arrow IPC {@code Message} table (root of every encapsulated IPC message).
+ */
+public final class Message extends Table {
+ /**
+ * Reads the {@code Message} located at the root offset of the supplied buffer.
+ *
+ * @param bb the little-endian buffer positioned at the start of the message.
+ * @return the {@code Message} accessor.
+ */
+ public static Message getRootAsMessage(ByteBuffer bb) {
+ return getRootAsMessage(bb, new Message());
+ }
+
+ /**
+ * Reads the {@code Message} located at the root offset of the supplied buffer into {@code obj}.
+ *
+ * @param bb the buffer positioned at the start of the message.
+ * @param obj the accessor instance to assign.
+ * @return the assigned {@code Message} accessor.
+ */
+ public static Message getRootAsMessage(ByteBuffer bb, Message obj) {
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ return obj.__assign(bb.getInt(bb.position()) + bb.position(), bb);
+ }
+
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Message __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the discriminator identifying the type of the {@code header} union (see {@link MessageHeader}).
+ *
+ * @return the header union type, or {@code 0} when absent.
+ */
+ public byte headerType() {
+ int o = __offset(6);
+ return o != 0 ? bb.get(o + bb_pos) : 0;
+ }
+
+ /**
+ * Resolves the {@code header} union value into the supplied accessor.
+ *
+ * @param obj the accessor to assign to the union value.
+ * @return the assigned accessor, or {@code null} when the header is absent.
+ */
+ public Table header(Table obj) {
+ int o = __offset(8);
+ return o != 0 ? __union(obj, o + bb_pos) : null;
+ }
+
+ /**
+ * Gets the length, in bytes, of the message body that follows the metadata.
+ *
+ * @return the body length, or {@code 0} when absent.
+ */
+ public long bodyLength() {
+ int o = __offset(10);
+ return o != 0 ? bb.getLong(o + bb_pos) : 0L;
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/MessageHeader.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/MessageHeader.java
new file mode 100644
index 000000000000..25e8c69da309
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/MessageHeader.java
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+/**
+ * Discriminator values for the Arrow IPC {@code MessageHeader} union.
+ */
+public final class MessageHeader {
+ private MessageHeader() {
+ }
+
+ /** No header. */
+ public static final byte NONE = 0;
+ /** A {@link Schema} header. */
+ public static final byte SCHEMA = 1;
+ /** A dictionary batch header. */
+ public static final byte DICTIONARY_BATCH = 2;
+ /** A {@link RecordBatch} header. */
+ public static final byte RECORD_BATCH = 3;
+ /** A tensor header. */
+ public static final byte TENSOR = 4;
+ /** A sparse tensor header. */
+ public static final byte SPARSE_TENSOR = 5;
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/RecordBatch.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/RecordBatch.java
new file mode 100644
index 000000000000..7afa0f3cde36
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/RecordBatch.java
@@ -0,0 +1,130 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code RecordBatch} table.
+ */
+public final class RecordBatch extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public RecordBatch __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the number of rows in the record batch.
+ *
+ * @return the row count, or {@code 0} when absent.
+ */
+ public long length() {
+ int o = __offset(4);
+ return o != 0 ? bb.getLong(o + bb_pos) : 0L;
+ }
+
+ /**
+ * Gets the field node at the given index.
+ *
+ * @param j the node index.
+ * @return the field node accessor.
+ */
+ public FieldNode nodes(int j) {
+ return nodes(new FieldNode(), j);
+ }
+
+ /**
+ * Gets the field node at the given index into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @param j the node index.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public FieldNode nodes(FieldNode obj, int j) {
+ int o = __offset(6);
+ return o != 0 ? obj.__assign(__vector(o) + j * 16, bb) : null;
+ }
+
+ /**
+ * Gets the number of field nodes.
+ *
+ * @return the field node count.
+ */
+ public int nodesLength() {
+ int o = __offset(6);
+ return o != 0 ? __vector_len(o) : 0;
+ }
+
+ /**
+ * Gets the buffer region at the given index.
+ *
+ * @param j the buffer index.
+ * @return the buffer accessor.
+ */
+ public Buffer buffers(int j) {
+ return buffers(new Buffer(), j);
+ }
+
+ /**
+ * Gets the buffer region at the given index into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @param j the buffer index.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public Buffer buffers(Buffer obj, int j) {
+ int o = __offset(8);
+ return o != 0 ? obj.__assign(__vector(o) + j * 16, bb) : null;
+ }
+
+ /**
+ * Gets the number of buffers.
+ *
+ * @return the buffer count.
+ */
+ public int buffersLength() {
+ int o = __offset(8);
+ return o != 0 ? __vector_len(o) : 0;
+ }
+
+ /**
+ * Gets the optional body compression descriptor.
+ *
+ * @return the body compression accessor, or {@code null} when the batch is uncompressed.
+ */
+ public BodyCompression compression() {
+ return compression(new BodyCompression());
+ }
+
+ /**
+ * Gets the optional body compression descriptor into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @return the assigned accessor, or {@code null} when the batch is uncompressed.
+ */
+ public BodyCompression compression(BodyCompression obj) {
+ int o = __offset(10);
+ return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null;
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Schema.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Schema.java
new file mode 100644
index 000000000000..1990ea7d8556
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Schema.java
@@ -0,0 +1,110 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code Schema} table.
+ */
+public final class Schema extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Schema __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the byte order of the schema's buffers (see {@link Endianness}).
+ *
+ * @return the endianness, or {@code 0} ({@link Endianness#LITTLE}) when absent.
+ */
+ public short endianness() {
+ int o = __offset(4);
+ return o != 0 ? bb.getShort(o + bb_pos) : 0;
+ }
+
+ /**
+ * Gets the field at the given index.
+ *
+ * @param j the field index.
+ * @return the field accessor.
+ */
+ public Field fields(int j) {
+ return fields(new Field(), j);
+ }
+
+ /**
+ * Gets the field at the given index into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @param j the field index.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public Field fields(Field obj, int j) {
+ int o = __offset(6);
+ return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null;
+ }
+
+ /**
+ * Gets the number of top-level fields in the schema.
+ *
+ * @return the field count.
+ */
+ public int fieldsLength() {
+ int o = __offset(6);
+ return o != 0 ? __vector_len(o) : 0;
+ }
+
+ /**
+ * Gets the custom metadata entry at the given index.
+ *
+ * @param j the entry index.
+ * @return the key/value accessor.
+ */
+ public KeyValue customMetadata(int j) {
+ return customMetadata(new KeyValue(), j);
+ }
+
+ /**
+ * Gets the custom metadata entry at the given index into the supplied accessor.
+ *
+ * @param obj the accessor to assign.
+ * @param j the entry index.
+ * @return the assigned accessor, or {@code null} when absent.
+ */
+ public KeyValue customMetadata(KeyValue obj, int j) {
+ int o = __offset(8);
+ return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null;
+ }
+
+ /**
+ * Gets the number of custom metadata entries.
+ *
+ * @return the entry count.
+ */
+ public int customMetadataLength() {
+ int o = __offset(8);
+ return o != 0 ? __vector_len(o) : 0;
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/TimeUnit.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/TimeUnit.java
new file mode 100644
index 000000000000..347e85337f9b
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/TimeUnit.java
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+/**
+ * Values for the Arrow IPC {@code TimeUnit} enum.
+ */
+public final class TimeUnit {
+ private TimeUnit() {
+ }
+
+ /** Second resolution. */
+ public static final short SECOND = 0;
+ /** Millisecond resolution. */
+ public static final short MILLISECOND = 1;
+ /** Microsecond resolution. */
+ public static final short MICROSECOND = 2;
+ /** Nanosecond resolution. */
+ public static final short NANOSECOND = 3;
+
+ private static final String[] NAMES = { "SECOND", "MILLISECOND", "MICROSECOND", "NANOSECOND" };
+
+ /**
+ * Gets the canonical Arrow name for a {@code TimeUnit} value, for diagnostic messages.
+ *
+ * @param e the time unit value.
+ * @return the canonical Arrow time unit name.
+ */
+ public static String name(int e) {
+ return NAMES[e];
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Timestamp.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Timestamp.java
new file mode 100644
index 000000000000..4be2b069813e
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Timestamp.java
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+import com.google.flatbuffers.Table;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Accessor for the Arrow IPC {@code Timestamp} type table.
+ */
+public final class Timestamp extends Table {
+ /**
+ * Positions this accessor at the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ */
+ public void __init(int i, ByteBuffer bb) {
+ __reset(i, bb);
+ }
+
+ /**
+ * Assigns this accessor to the given table offset.
+ *
+ * @param i the table offset.
+ * @param bb the backing buffer.
+ * @return this accessor.
+ */
+ public Timestamp __assign(int i, ByteBuffer bb) {
+ __init(i, bb);
+ return this;
+ }
+
+ /**
+ * Gets the timestamp resolution (see {@link TimeUnit}).
+ *
+ * @return the time unit, or {@code 0} ({@link TimeUnit#SECOND}) when absent.
+ */
+ public short unit() {
+ int o = __offset(4);
+ return o != 0 ? bb.getShort(o + bb_pos) : 0;
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Type.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Type.java
new file mode 100644
index 000000000000..615d0d2ce1f5
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/Type.java
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util.arrow;
+
+/**
+ * Discriminator values for the Arrow IPC {@code Type} union, identifying a field's logical type.
+ */
+public final class Type {
+ private Type() {
+ }
+
+ /** No type. */
+ public static final byte NONE = 0;
+ /** Null type. */
+ public static final byte NULL = 1;
+ /** Integer type (see {@link Int}). */
+ public static final byte INT = 2;
+ /** Floating-point type. */
+ public static final byte FLOATING_POINT = 3;
+ /** Variable-length binary type. */
+ public static final byte BINARY = 4;
+ /** Variable-length UTF-8 string type. */
+ public static final byte UTF8 = 5;
+ /** Boolean type. */
+ public static final byte BOOL = 6;
+ /** Decimal type. */
+ public static final byte DECIMAL = 7;
+ /** Date type. */
+ public static final byte DATE = 8;
+ /** Time-of-day type. */
+ public static final byte TIME = 9;
+ /** Timestamp type (see {@link Timestamp}). */
+ public static final byte TIMESTAMP = 10;
+ /** Interval type. */
+ public static final byte INTERVAL = 11;
+ /** List type. */
+ public static final byte LIST = 12;
+ /** Struct type. */
+ public static final byte STRUCT = 13;
+ /** Union type. */
+ public static final byte UNION = 14;
+ /** Fixed-size binary type. */
+ public static final byte FIXED_SIZE_BINARY = 15;
+ /** Fixed-size list type. */
+ public static final byte FIXED_SIZE_LIST = 16;
+ /** Map type. */
+ public static final byte MAP = 17;
+ /** Duration type. */
+ public static final byte DURATION = 18;
+ /** Large variable-length binary type. */
+ public static final byte LARGE_BINARY = 19;
+ /** Large variable-length UTF-8 string type. */
+ public static final byte LARGE_UTF8 = 20;
+ /** Large list type. */
+ public static final byte LARGE_LIST = 21;
+ /** Run-end encoded type. */
+ public static final byte RUN_END_ENCODED = 22;
+ /** Binary view type. */
+ public static final byte BINARY_VIEW = 23;
+ /** UTF-8 string view type. */
+ public static final byte UTF8_VIEW = 24;
+ /** List view type. */
+ public static final byte LIST_VIEW = 25;
+ /** Large list view type. */
+ public static final byte LARGE_LIST_VIEW = 26;
+
+ private static final String[] NAMES = {
+ "NONE", "Null", "Int", "FloatingPoint", "Binary", "Utf8", "Bool", "Decimal", "Date", "Time", "Timestamp",
+ "Interval", "List", "Struct_", "Union", "FixedSizeBinary", "FixedSizeList", "Map", "Duration", "LargeBinary",
+ "LargeUtf8", "LargeList", "RunEndEncoded", "BinaryView", "Utf8View", "ListView", "LargeListView"
+ };
+
+ /**
+ * Gets the canonical Arrow name for a {@code Type} union discriminator, for diagnostic messages.
+ *
+ * @param e the discriminator value.
+ * @return the canonical Arrow type name.
+ */
+ public static String name(int e) {
+ return NAMES[e];
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/package-info.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/package-info.java
new file mode 100644
index 000000000000..ce929799031f
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/arrow/package-info.java
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+/**
+ * Minimal Apache Arrow IPC FlatBuffer metadata accessors used internally by the Blob Storage ListBlobs Arrow reader.
+ *
+ * The classes in this package are thin {@link com.google.flatbuffers.Table}/{@link com.google.flatbuffers.Struct}
+ * accessors over the FlatBuffer-encoded metadata of the Apache Arrow IPC format. They expose only the small subset of
+ * the {@code org.apache.arrow.flatbuf} schema that {@code BlobListArrowStreamReader} requires, so that the main
+ * (Java 8 baseline) compile classpath does not depend on the {@code arrow-format} artifact, which ships Java 11
+ * bytecode.
+ *
+ * The field orderings (FlatBuffer vtable slots) and enum values are defined by the public Apache Arrow columnar format
+ * specification (see {@code Schema.fbs} and {@code Message.fbs} in the Apache Arrow project, licensed under the Apache
+ * License, Version 2.0) and must match the on-the-wire layout produced by the Storage service.
+ */
+package com.azure.storage.blob.implementation.util.arrow;
+
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
index 6a372f5b2c74..dfd93a535fc6 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/BlobDownloadHeaders.java
@@ -825,97 +825,6 @@ public BlobDownloadHeaders setEncryptionScope(String encryptionScope) {
return this;
}
- /**
- * Gets the access tier of the blob.
- *
- * @return the access tier of the blob. This is only set for Page blobs on a premium storage account or for Block
- * blobs on blob storage or general purpose V2 account.
- */
- public AccessTier getAccessTier() {
- String accessTier = internalHeaders.getXMsAccessTier();
- return accessTier == null ? null : AccessTier.fromString(accessTier);
- }
-
- /**
- * Sets the access tier of the blob.
- *
- * @param accessTier the access tier of the blob.
- * @return the BlobDownloadHeaders object itself.
- */
- public BlobDownloadHeaders setAccessTier(AccessTier accessTier) {
- internalHeaders.setXMsAccessTier(accessTier == null ? null : accessTier.toString());
- return this;
- }
-
- /**
- * Gets the status of the tier being inferred for the blob.
- *
- * @return the status of the tier being inferred for the blob. This is only set for Page blobs on a premium storage
- * account or for Block blobs on blob storage or general purpose V2 account.
- */
- public Boolean isAccessTierInferred() {
- return Boolean.TRUE.equals(internalHeaders.isXMsAccessTierInferred());
- }
-
- /**
- * Sets the status of the tier being inferred for the blob.
- *
- * @param accessTierInferred the status of the tier being inferred for the blob.
- * @return the BlobDownloadHeaders object itself.
- */
- public BlobDownloadHeaders setAccessTierInferred(Boolean accessTierInferred) {
- internalHeaders.setXMsAccessTierInferred(accessTierInferred);
- return this;
- }
-
- /**
- * Gets the time when the access tier for the blob was last changed.
- *
- * @return the time when the access tier for the blob was last changed.
- */
- public OffsetDateTime getAccessTierChangeTime() {
- return internalHeaders.getXMsAccessTierChangeTime();
- }
-
- /**
- * Sets the time when the access tier for the blob was last changed.
- *
- * @param accessTierChangeTime the time when the access tier for the blob was last changed.
- * @return the BlobDownloadHeaders object itself.
- */
- public BlobDownloadHeaders setAccessTierChangeTime(OffsetDateTime accessTierChangeTime) {
- internalHeaders.setXMsAccessTierChangeTime(accessTierChangeTime);
- return this;
- }
-
- /**
- * Gets the underlying access tier of the blob when its access tier is {@link AccessTier#SMART}.
- *
- * This value is only populated when {@link #getAccessTier()} returns {@link AccessTier#SMART}. In that case, it
- * represents the concrete access tier (for example {@link AccessTier#HOT} or {@link AccessTier#COOL}) that the
- * service has selected for the blob. For all other access tiers, this property is {@code null} and should be
- * ignored.
- *
- * @return the underlying access tier chosen by the service when the blob's access tier is {@link AccessTier#SMART},
- * or {@code null} if the blob is not using the smart access tier.
- */
- public AccessTier getSmartAccessTier() {
- String smartAccessTier = internalHeaders.getXMsSmartAccessTier();
- return smartAccessTier == null ? null : AccessTier.fromString(smartAccessTier);
- }
-
- /**
- * Sets the underlying access tier of the blob when its access tier is {@link AccessTier#SMART}.
- *
- * @param smartAccessTier the underlying access tier chosen by the service when the blob's access tier is
- * {@link AccessTier#SMART}.
- * @return the BlobDownloadHeaders object itself.
- */
- public BlobDownloadHeaders setSmartAccessTier(AccessTier smartAccessTier) {
- internalHeaders.setXMsSmartAccessTier(smartAccessTier == null ? null : smartAccessTier.toString());
- return this;
- }
-
/**
* Get the blobContentMD5 property: If the blob has a MD5 hash, and if request contains range header (Range or
* x-ms-range), this response header is returned with the value of the whole blob's MD5 value. This value may or may
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java
index 47f29c2ab2b3..e434681ac6e9 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/ListBlobsOptions.java
@@ -19,6 +19,8 @@ public final class ListBlobsOptions {
private String prefix;
private String startFrom;
private Integer maxResultsPerPage;
+ private StorageResponseSerializationFormat storageResponseSerializationFormat;
+ private String endBefore;
/**
* Constructs an unpopulated {@link ListBlobsOptions}.
@@ -74,7 +76,7 @@ public ListBlobsOptions setPrefix(String prefix) {
* This parameter is similar to the prefix filter: it allows listing blobs starting from the specified path, rather than from the beginning of the container.
* For non-recursive lists, only one entity level is supported.
*
- * @return the marker indicating where to start listing blobs
+ * @return the marker indicating where to start listing blobs (inclusive)
*/
public String getStartFrom() {
return startFrom;
@@ -84,7 +86,7 @@ public String getStartFrom() {
* Sets an optional parameter that specifies an absolute path within the container. This parameter is similar to the prefix filter: it allows listing blobs starting from the specified path, rather than from the beginning of the container.
* For non-recursive lists, only one entity level is supported.
*
- * @param startFrom The marker indicating where to start listing blobs
+ * @param startFrom The marker indicating where to start listing blobs (inclusive)
* @return the updated ListBlobsOptions object
*/
public ListBlobsOptions setStartFrom(String startFrom) {
@@ -92,6 +94,49 @@ public ListBlobsOptions setStartFrom(String startFrom) {
return this;
}
+ /**
+ * Gets the endBefore value. Only supported with Arrow listings. The listing will end before this path (exclusive).
+ *
+ * @return the endBefore value.
+ */
+ public String getEndBefore() {
+ return endBefore;
+ }
+
+ /**
+ * Sets the endBefore value. Only supported with Arrow listings. The listing will end before this path (exclusive).
+ *
+ * @param endBefore the endBefore value to set.
+ * @return the updated ListBlobsOptions object.
+ */
+ public ListBlobsOptions setEndBefore(String endBefore) {
+ this.endBefore = endBefore;
+ return this;
+ }
+
+ /**
+ * Gets the response serialization format the service should use when listing blobs.
+ *
+ * @return the {@link StorageResponseSerializationFormat}, or {@code null} if unset
+ * (equivalent to {@link StorageResponseSerializationFormat#AUTO}).
+ */
+ public StorageResponseSerializationFormat getStorageResponseSerializationFormat() {
+ return storageResponseSerializationFormat;
+ }
+
+ /**
+ * Sets the response serialization format the service should use when listing blobs.
+ *
+ * @param storageResponseSerializationFormat the format to request. {@code null} and
+ * {@link StorageResponseSerializationFormat#AUTO} both let the SDK pick.
+ * @return the updated {@link ListBlobsOptions} object.
+ */
+ public ListBlobsOptions
+ setStorageResponseSerializationFormat(StorageResponseSerializationFormat storageResponseSerializationFormat) {
+ this.storageResponseSerializationFormat = storageResponseSerializationFormat;
+ return this;
+ }
+
/**
* Specifies the maximum number of blobs to return, including all BlobPrefix elements. If the request does not
* specify maxResultsPerPage or specifies a value greater than 5,000, the server will return up to 5,000 items.
diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/StorageResponseSerializationFormat.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/StorageResponseSerializationFormat.java
new file mode 100644
index 000000000000..e5017accedbc
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/models/StorageResponseSerializationFormat.java
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.models;
+
+/**
+ * Defines the serialization format the service uses for list-blobs responses.
+ */
+public enum StorageResponseSerializationFormat {
+ /**
+ * Let the SDK choose the serialization format that is most appropriate for the request.
+ *
+ * The exact format selected by {@code AUTO} is an implementation detail and may change
+ * between SDK releases. Choose {@link #XML} or {@link #ARROW} explicitly if you require
+ * a specific format.
+ */
+ AUTO,
+
+ /**
+ * XML response format.
+ */
+ XML,
+
+ /**
+ * Apache Arrow response format.
+ */
+ ARROW
+}
diff --git a/sdk/storage/azure-storage-blob/src/main/java/module-info.java b/sdk/storage/azure-storage-blob/src/main/java/module-info.java
index 597a417add7f..e6947c9af0c4 100644
--- a/sdk/storage/azure-storage-blob/src/main/java/module-info.java
+++ b/sdk/storage/azure-storage-blob/src/main/java/module-info.java
@@ -5,6 +5,7 @@
requires transitive com.azure.storage.common;
requires com.azure.storage.internal.avro;
+ requires flatbuffers.java;
exports com.azure.storage.blob;
exports com.azure.storage.blob.models;
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java
index 71c474ba295c..50a9eb63ef21 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobApiTests.java
@@ -543,36 +543,6 @@ public void downloadAllNullBinaryData() {
// headers.getLastAccessedTime() /* TODO (gapra): re-enable when last access time enabled. */
}
- @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06")
- @Test
- public void downloadSmartAccessTierHeaders() {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- bc.setAccessTier(AccessTier.SMART);
-
- BlobDownloadResponse response = bc.downloadStreamWithResponse(stream, null, null, null, false, null, null);
- ByteBuffer body = ByteBuffer.wrap(stream.toByteArray());
-
- assertEquals(DATA.getDefaultData(), body);
- assertSmartAccessTierHeaders(response.getDeserializedHeaders());
- }
-
- @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06")
- @Test
- public void downloadContentSmartAccessTierHeaders() {
- bc.setAccessTier(AccessTier.SMART);
- BlobDownloadContentResponse response = bc.downloadContentWithResponse(null, null, null, null);
-
- TestUtils.assertArraysEqual(DATA.getDefaultBytes(), response.getValue().toBytes());
- assertSmartAccessTierHeaders(response.getDeserializedHeaders());
- }
-
- private static void assertSmartAccessTierHeaders(BlobDownloadHeaders headers) {
- assertEquals(AccessTier.SMART, headers.getAccessTier());
- assertNotNull(headers.getSmartAccessTier());
- assertFalse(headers.isAccessTierInferred());
- assertNotEquals(OffsetDateTime.now(), headers.getAccessTierChangeTime());
- }
-
@Test
public void downloadEmptyFile() {
AppendBlobClient bc = cc.getBlobClient("emptyAppendBlob").getAppendBlobClient();
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java
index ea01df338d18..049e4254e92a 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobAsyncApiTests.java
@@ -382,33 +382,6 @@ public void downloadAllNullBinaryData() {
.verifyComplete();
}
- @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-10-06")
- @Test
- public void downloadSmartAccessTierHeaders() {
- Mono response = bc.setAccessTier(AccessTier.SMART)
- .then(bc.downloadStreamWithResponse(null, null, null, false))
- .flatMap(r -> {
- assertSmartAccessTierHeaders(r.getDeserializedHeaders());
- return FluxUtil.collectBytesInByteBufferStream(r.getValue());
- })
- .flatMap(r -> {
- TestUtils.assertArraysEqual(DATA.getDefaultBytes(), r);
- return bc.downloadContentWithResponse(null, null);
- });
-
- StepVerifier.create(response).assertNext(r -> {
- assertSmartAccessTierHeaders(r.getDeserializedHeaders());
- TestUtils.assertArraysEqual(DATA.getDefaultBytes(), r.getValue().toBytes());
- }).verifyComplete();
- }
-
- private static void assertSmartAccessTierHeaders(BlobDownloadHeaders headers) {
- assertEquals(AccessTier.SMART, headers.getAccessTier());
- assertNotNull(headers.getSmartAccessTier());
- assertFalse(headers.isAccessTierInferred());
- assertNotEquals(OffsetDateTime.now(), headers.getAccessTierChangeTime());
- }
-
@Test
public void downloadEmptyFile() {
AppendBlobAsyncClient bc = ccAsync.getBlobAsyncClient("emptyAppendBlob").getAppendBlobAsyncClient();
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java
index f46116acdbb5..54ae007c7165 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java
@@ -34,6 +34,7 @@
import com.azure.storage.blob.models.PublicAccessType;
import com.azure.storage.blob.models.RehydratePriority;
import com.azure.storage.blob.models.StorageAccountInfo;
+import com.azure.storage.blob.models.StorageResponseSerializationFormat;
import com.azure.storage.blob.models.TaggedBlobItem;
import com.azure.storage.blob.options.BlobContainerCreateOptions;
import com.azure.storage.blob.options.BlobParallelUploadOptions;
@@ -50,6 +51,10 @@
import com.azure.storage.common.test.shared.extensions.PlaybackOnly;
import com.azure.storage.common.test.shared.extensions.RequiredServiceVersion;
import com.azure.storage.common.test.shared.policy.InvalidServiceVersionPipelinePolicy;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.ipc.ArrowStreamReader;
+import org.apache.arrow.vector.VectorSchemaRoot;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -58,7 +63,19 @@
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
+import com.azure.core.http.HttpPipeline;
+import com.azure.storage.blob.implementation.AzureBlobStorageImpl;
+import com.azure.storage.blob.implementation.AzureBlobStorageImplBuilder;
+import com.azure.storage.blob.implementation.models.ContainersListBlobFlatSegmentApacheArrowHeaders;
+import com.azure.storage.blob.implementation.models.ContainersListBlobHierarchySegmentApacheArrowHeaders;
+import com.azure.storage.blob.implementation.util.ArrowBlobListDeserializer;
+import com.azure.storage.blob.implementation.util.ModelHelper;
+import com.azure.storage.blob.models.ListBlobsIncludeItem;
+import com.azure.core.http.rest.ResponseBase;
+
import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
import java.net.URL;
import java.time.OffsetDateTime;
import java.util.Arrays;
@@ -2128,4 +2145,313 @@ public void getBlobContainerUrlEncodesContainerName() {
// then:
// assertThrows(BlobStorageException.class, () ->
// }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowBasic() {
+ // Upload a test blob
+ String blobName = generateBlobName();
+ cc.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+ List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList());
+
+ assertEquals(1, blobs.size());
+ assertEquals(blobName, blobs.get(0).getName());
+ assertNotNull(blobs.get(0).getProperties());
+ assertEquals(DATA.getDefaultDataSize(), blobs.get(0).getProperties().getContentLength());
+ assertEquals(BlobType.BLOCK_BLOB, blobs.get(0).getProperties().getBlobType());
+ assertNotNull(blobs.get(0).getProperties().getLastModified());
+ assertNotNull(blobs.get(0).getProperties().getETag());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowWithTags() {
+ // Upload a blob and set tags
+ String blobName = generateBlobName();
+ cc.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ Map tags = new HashMap<>();
+ tags.put("tagkey", "tagvalue");
+ cc.getBlobClient(blobName).setTags(tags);
+
+ // List with Arrow + retrieveTags
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setDetails(new BlobListDetails().setRetrieveTags(true));
+ List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList());
+
+ assertEquals(1, blobs.size());
+ assertEquals(blobName, blobs.get(0).getName());
+ assertNotNull(blobs.get(0).getTags());
+ assertEquals("tagvalue", blobs.get(0).getTags().get("tagkey"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("listBlobsFlatRehydratePrioritySupplier")
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowRehydratePriority(RehydratePriority rehydratePriority) {
+ String name = generateBlobName();
+ BlockBlobClient bc = cc.getBlobClient(name).getBlockBlobClient();
+
+ bc.upload(DATA.getDefaultInputStream(), 7);
+
+ if (rehydratePriority != null) {
+ bc.setAccessTier(AccessTier.ARCHIVE);
+ bc.setAccessTierWithResponse(new BlobSetAccessTierOptions(AccessTier.HOT).setPriority(rehydratePriority),
+ null, null);
+ }
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+ BlobItem item = cc.listBlobs(options, null).iterator().next();
+
+ assertEquals(rehydratePriority, item.getProperties().getRehydratePriority());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowWithMetadata() {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+ cc.getBlobClient(blobName)
+ .getBlockBlobClient()
+ .uploadWithResponse(DATA.getDefaultInputStream(), DATA.getDefaultDataSize(), null, metadata, null, null,
+ null, null, null);
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setDetails(new BlobListDetails().setRetrieveMetadata(true));
+ List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList());
+
+ assertEquals(1, blobs.size());
+ assertNotNull(blobs.get(0).getMetadata());
+ assertEquals("testvalue", blobs.get(0).getMetadata().get("testkey"));
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowPagination() {
+ // Upload 3 blobs
+ for (int i = 0; i < 4; i++) {
+ cc.getBlobClient("blob" + i)
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+ }
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setMaxResultsPerPage(1);
+ List allBlobs = new ArrayList<>();
+ for (PagedResponse page : cc.listBlobs(options, null).iterableByPage()) {
+ assertTrue(page.getValue().size() <= 1);
+ allBlobs.addAll(page.getValue());
+ }
+
+ cc.listBlobs().iterableByPage(2).forEach(page -> {
+ assertEquals(2, page.getValue().size());
+ });
+
+ assertEquals(4, allBlobs.size());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowNullUseArrowUsesXml() {
+ // Default apacheArrowEnabled is null — should use XML path without error
+ String blobName = generateBlobName();
+ cc.getBlobClient(blobName).getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options = new ListBlobsOptions();
+ assertNull(options.getStorageResponseSerializationFormat());
+
+ List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList());
+ assertEquals(1, blobs.size());
+ assertEquals(blobName, blobs.get(0).getName());
+ }
+
+ @LiveOnly
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowEncryptedBlob() {
+ // Upload a blob with CPK (customer-provided key)
+ String blobName = generateBlobName();
+ CustomerProvidedKey cpk = new CustomerProvidedKey(Base64.getEncoder().encodeToString(getRandomKey()));
+ BlobClient cpkClient = cc.getBlobClient(blobName).getCustomerProvidedKeyClient(cpk);
+ cpkClient.getBlockBlobClient().upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+ List blobs = cc.listBlobs(options, null).stream().collect(Collectors.toList());
+
+ assertEquals(1, blobs.size());
+ assertEquals(blobName, blobs.get(0).getName());
+ // CPK blob should have server-encrypted = true
+ assertTrue(blobs.get(0).getProperties().isServerEncrypted());
+ // Metadata should be null (no metadata was set)
+ assertNull(blobs.get(0).getMetadata());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowDeserializer() throws Exception {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+ cc.getBlobClient(blobName)
+ .getBlockBlobClient()
+ .uploadWithResponse(DATA.getDefaultInputStream(), 7, null, metadata, null, null, null, null, null);
+
+ AzureBlobStorageImpl impl = new AzureBlobStorageImplBuilder().pipeline(cc.getHttpPipeline())
+ .url(cc.getAccountUrl())
+ .version(BlobServiceVersion.getLatest().getVersion())
+ .buildClient();
+
+ // Call the Arrow endpoint
+ ArrayList include = new ArrayList<>();
+ include.add(ListBlobsIncludeItem.METADATA);
+
+ ResponseBase response = impl.getContainers()
+ .listBlobFlatSegmentApacheArrowWithResponse(containerName, null, null, null, include, null, null, null,
+ null, com.azure.core.util.Context.NONE);
+
+ // Verify Content-Type is Arrow
+ String contentType = response.getDeserializedHeaders().getContentType();
+ assertTrue(contentType.contains("application/vnd.apache.arrow.stream"),
+ "Expected Arrow content type but got: " + contentType);
+
+ // Deserialize using ArrowBlobListDeserializer
+ ArrowBlobListDeserializer.ArrowListBlobsResult result
+ = ArrowBlobListDeserializer.deserialize(response.getValue());
+
+ // Verify pagination — single blob, no next page
+ assertNull(result.getNextMarker());
+
+ // Verify we got exactly one blob
+ assertEquals(1, result.getBlobItems().size());
+
+ com.azure.storage.blob.implementation.models.BlobItemInternal item = result.getBlobItems().get(0);
+
+ // Name
+ assertNotNull(item.getName());
+ assertEquals(blobName, item.getName().getContent());
+
+ // Properties
+ assertNotNull(item.getProperties());
+ assertEquals(7L, (long) item.getProperties().getContentLength());
+ assertEquals("application/octet-stream", item.getProperties().getContentType());
+ assertNotNull(item.getProperties().getETag());
+ assertNotNull(item.getProperties().getLastModified());
+ assertNotNull(item.getProperties().getCreationTime());
+ assertEquals(BlobType.BLOCK_BLOB, item.getProperties().getBlobType());
+ assertEquals(AccessTier.HOT, item.getProperties().getAccessTier());
+ assertTrue(item.getProperties().isAccessTierInferred());
+ assertTrue(item.getProperties().isServerEncrypted());
+ assertEquals(LeaseStateType.AVAILABLE, item.getProperties().getLeaseState());
+ assertEquals(LeaseStatusType.UNLOCKED, item.getProperties().getLeaseStatus());
+ assertNotNull(item.getProperties().getContentMd5());
+
+ // Metadata
+ assertNotNull(item.getMetadata());
+ assertEquals("testvalue", item.getMetadata().get("testkey"));
+
+ // Verify ModelHelper can convert to public BlobItem
+ BlobItem publicItem = ModelHelper.populateBlobItem(item);
+ assertEquals(blobName, publicItem.getName());
+ assertEquals(7L, (long) publicItem.getProperties().getContentLength());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowBasic() {
+ // Upload blobs in a directory structure
+ cc.getBlobClient("dir/blob1")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+ cc.getBlobClient("dir/blob2")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+ cc.getBlobClient("topblob")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+ List items = cc.listBlobsByHierarchy("/", options, null).stream().collect(Collectors.toList());
+
+ // Root level: one prefix "dir/" and one blob "topblob"
+ assertEquals(2, items.size());
+
+ BlobItem prefixItem = items.stream().filter(BlobItem::isPrefix).findFirst().orElse(null);
+ BlobItem blobItem = items.stream().filter(i -> !i.isPrefix()).findFirst().orElse(null);
+
+ assertNotNull(prefixItem);
+ assertEquals("dir/", prefixItem.getName());
+ assertTrue(prefixItem.isPrefix());
+
+ assertNotNull(blobItem);
+ assertEquals("topblob", blobItem.getName());
+ assertFalse(blobItem.isPrefix());
+ assertNotNull(blobItem.getProperties());
+ assertEquals(DATA.getDefaultDataSize(), blobItem.getProperties().getContentLength());
+ assertEquals(BlobType.BLOCK_BLOB, blobItem.getProperties().getBlobType());
+ assertNotNull(blobItem.getProperties().getLastModified());
+ assertNotNull(blobItem.getProperties().getETag());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowWithMetadata() {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+ cc.getBlobClient("dir/" + blobName)
+ .getBlockBlobClient()
+ .uploadWithResponse(DATA.getDefaultInputStream(), DATA.getDefaultDataSize(), null, metadata, null, null,
+ null, null, null);
+ cc.getBlobClient("topblob")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setPrefix("dir/")
+ .setDetails(new BlobListDetails().setRetrieveMetadata(true));
+ List blobs = cc.listBlobsByHierarchy("/", options, null).stream().collect(Collectors.toList());
+
+ assertEquals(1, blobs.size());
+ assertFalse(blobs.get(0).isPrefix());
+ assertNotNull(blobs.get(0).getMetadata());
+ assertEquals("testvalue", blobs.get(0).getMetadata().get("testkey"));
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowPagination() {
+ // Upload blobs across multiple directories
+ for (int i = 0; i < 3; i++) {
+ cc.getBlobClient("dir" + i + "/blob")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+ }
+ cc.getBlobClient("topblob")
+ .getBlockBlobClient()
+ .upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize());
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setMaxResultsPerPage(1);
+ List allItems = new ArrayList<>();
+ for (PagedResponse page : cc.listBlobsByHierarchy("/", options, null).iterableByPage()) {
+ assertTrue(page.getValue().size() <= 1);
+ allItems.addAll(page.getValue());
+ }
+
+ // 3 prefixes + 1 blob = 4 items
+ assertEquals(4, allItems.size());
+ }
+
}
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java
index 04ebc06dc2b6..6e574cda0e27 100644
--- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java
@@ -10,8 +10,14 @@
import com.azure.core.test.TestMode;
import com.azure.core.test.utils.MockTokenCredential;
import com.azure.core.util.Context;
+import com.azure.core.util.FluxUtil;
import com.azure.core.util.polling.PollerFlux;
import com.azure.identity.DefaultAzureCredentialBuilder;
+import com.azure.storage.blob.implementation.AzureBlobStorageImpl;
+import com.azure.storage.blob.implementation.AzureBlobStorageImplBuilder;
+import com.azure.storage.blob.implementation.models.BlobItemInternal;
+import com.azure.storage.blob.implementation.util.ArrowBlobListDeserializer;
+import com.azure.storage.blob.implementation.util.ModelHelper;
import com.azure.storage.blob.models.*;
import com.azure.storage.blob.options.BlobContainerCreateOptions;
import com.azure.storage.blob.options.BlobParallelUploadOptions;
@@ -41,6 +47,7 @@
import reactor.util.function.Tuple2;
import java.net.URL;
+import java.io.ByteArrayInputStream;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.*;
@@ -2142,4 +2149,334 @@ public void getBlobContainerUrlEncodesContainerName() {
assertTrue(containerClient.getBlobContainerUrl().contains("my%20container"));
}
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowBasic() {
+ // Upload a test blob
+ String blobName = generateBlobName();
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+
+ StepVerifier
+ .create(
+ bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()).thenMany(ccAsync.listBlobs(options, null)))
+ .assertNext(item -> {
+ assertEquals(blobName, item.getName());
+ assertNotNull(item.getProperties());
+ assertEquals(DATA.getDefaultDataSize(), item.getProperties().getContentLength());
+ assertEquals(BlobType.BLOCK_BLOB, item.getProperties().getBlobType());
+ assertNotNull(item.getProperties().getLastModified());
+ assertNotNull(item.getProperties().getETag());
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowWithMetadata() {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setDetails(new BlobListDetails().setRetrieveMetadata(true));
+
+ StepVerifier.create(
+ bc.uploadWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, metadata, null, null, null)
+ .thenMany(ccAsync.listBlobs(options, null)))
+ .assertNext(item -> {
+ assertNotNull(item.getMetadata());
+ assertEquals("testvalue", item.getMetadata().get("testkey"));
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowPagination() {
+ // Upload 4 blobs
+ Flux uploads = Flux.range(0, 4)
+ .flatMap(i -> ccAsync.getBlobAsyncClient("blob" + i)
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()));
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setMaxResultsPerPage(1);
+
+ Mono> result = uploads.then(ccAsync.listBlobs(options, null).byPage().doOnNext(page -> {
+ assertTrue(page.getValue().size() <= 1);
+ }).flatMap(page -> Flux.fromIterable(page.getValue())).collectList());
+
+ StepVerifier.create(result).assertNext(allBlobs -> assertEquals(4, allBlobs.size())).verifyComplete();
+
+ // Mirror the sync test's secondary assertion: requesting page size 2 yields exactly 2 blobs per page.
+ StepVerifier.create(ccAsync.listBlobs().byPage(2)).thenConsumeWhile(page -> {
+ assertEquals(2, page.getValue().size());
+ return true;
+ }).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowNullUseArrowUsesXml() {
+ // Default apacheArrowEnabled is null — should use XML path without error
+ String blobName = generateBlobName();
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();
+
+ ListBlobsOptions options = new ListBlobsOptions();
+ assertNull(options.getStorageResponseSerializationFormat());
+
+ StepVerifier
+ .create(
+ bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()).thenMany(ccAsync.listBlobs(options, null)))
+ .assertNext(item -> assertEquals(blobName, item.getName()))
+ .verifyComplete();
+ }
+
+ @LiveOnly
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowEncryptedBlob() {
+ // Upload a blob with CPK (customer-provided key)
+ String blobName = generateBlobName();
+ CustomerProvidedKey cpk = new CustomerProvidedKey(Base64.getEncoder().encodeToString(getRandomKey()));
+ BlockBlobAsyncClient cpkClient
+ = ccAsync.getBlobAsyncClient(blobName).getCustomerProvidedKeyAsyncClient(cpk).getBlockBlobAsyncClient();
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+
+ StepVerifier.create(cpkClient.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())
+ .thenMany(ccAsync.listBlobs(options, null))).assertNext(item -> {
+ assertEquals(blobName, item.getName());
+ // CPK blob should have server-encrypted = true
+ assertTrue(item.getProperties().isServerEncrypted());
+ // Metadata should be null (no metadata was set)
+ assertNull(item.getMetadata());
+ }).verifyComplete();
+ }
+
+ @ParameterizedTest
+ @MethodSource("listBlobsFlatRehydratePrioritySupplier")
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowRehydratePriority(RehydratePriority rehydratePriority) {
+ String name = generateBlobName();
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(name).getBlockBlobAsyncClient();
+
+ Mono> rehydrate = Mono.empty();
+
+ if (rehydratePriority != null) {
+ rehydrate = bc.setAccessTier(AccessTier.ARCHIVE)
+ .then(bc.setAccessTierWithResponse(
+ new BlobSetAccessTierOptions(AccessTier.HOT).setPriority(rehydratePriority)));
+ }
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+
+ Flux response = bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())
+ .then(rehydrate)
+ .thenMany(ccAsync.listBlobs(options, null));
+
+ StepVerifier.create(response)
+ .assertNext(r -> assertEquals(rehydratePriority, r.getProperties().getRehydratePriority()))
+ .verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowDeserializer() {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();
+
+ AzureBlobStorageImpl impl = new AzureBlobStorageImplBuilder().pipeline(ccAsync.getHttpPipeline())
+ .url(ccAsync.getAccountUrl())
+ .version(BlobServiceVersion.getLatest().getVersion())
+ .buildClient();
+
+ List include = new ArrayList<>();
+ include.add(ListBlobsIncludeItem.METADATA);
+
+ Mono testMono
+ = bc.uploadWithResponse(DATA.getDefaultFlux(), 7, null, metadata, null, null, null)
+ .then(impl.getContainers()
+ .listBlobFlatSegmentApacheArrowWithResponseAsync(containerName, null, null, null, include, null,
+ null, null, null))
+ .flatMap(response -> {
+ // Verify Content-Type is Arrow
+ String contentType = response.getDeserializedHeaders().getContentType();
+ assertTrue(contentType.contains("application/vnd.apache.arrow.stream"),
+ "Expected Arrow content type but got: " + contentType);
+
+ // Collect the Flux body into a byte[] and feed it to the deserializer.
+ return FluxUtil.collectBytesInByteBufferStream(response.getValue())
+ .map(bytes -> ArrowBlobListDeserializer.deserialize(new ByteArrayInputStream(bytes)));
+ });
+
+ StepVerifier.create(testMono).assertNext(result -> {
+ // Verify pagination — single blob, no next page
+ assertNull(result.getNextMarker());
+
+ // Verify we got exactly one blob
+ assertEquals(1, result.getBlobItems().size());
+
+ BlobItemInternal item = result.getBlobItems().get(0);
+
+ // Name
+ assertNotNull(item.getName());
+ assertEquals(blobName, item.getName().getContent());
+
+ // Properties
+ assertNotNull(item.getProperties());
+ assertEquals(7L, (long) item.getProperties().getContentLength());
+ assertEquals("application/octet-stream", item.getProperties().getContentType());
+ assertNotNull(item.getProperties().getETag());
+ assertNotNull(item.getProperties().getLastModified());
+ assertNotNull(item.getProperties().getCreationTime());
+ assertEquals(BlobType.BLOCK_BLOB, item.getProperties().getBlobType());
+ assertEquals(AccessTier.HOT, item.getProperties().getAccessTier());
+ assertTrue(item.getProperties().isAccessTierInferred());
+ assertTrue(item.getProperties().isServerEncrypted());
+ assertEquals(LeaseStateType.AVAILABLE, item.getProperties().getLeaseState());
+ assertEquals(LeaseStatusType.UNLOCKED, item.getProperties().getLeaseStatus());
+ assertNotNull(item.getProperties().getContentMd5());
+
+ // Metadata
+ assertNotNull(item.getMetadata());
+ assertEquals("testvalue", item.getMetadata().get("testkey"));
+
+ // Verify ModelHelper can convert to public BlobItem
+ BlobItem publicItem = ModelHelper.populateBlobItem(item);
+ assertEquals(blobName, publicItem.getName());
+ assertEquals(7L, (long) publicItem.getProperties().getContentLength());
+ }).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowBasic() {
+ // Upload blobs in a directory structure
+ Flux uploads = Flux.concat(
+ ccAsync.getBlobAsyncClient("dir/blob1")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()),
+ ccAsync.getBlobAsyncClient("dir/blob2")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()),
+ ccAsync.getBlobAsyncClient("topblob")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()));
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW);
+
+ Mono> items
+ = uploads.then(ccAsync.listBlobsByHierarchy("/", options).collect(Collectors.toList()));
+
+ StepVerifier.create(items).assertNext(list -> {
+ // Root level: one prefix "dir/" and one blob "topblob"
+ assertEquals(2, list.size());
+
+ BlobItem prefixItem = list.stream().filter(BlobItem::isPrefix).findFirst().orElse(null);
+ BlobItem blobItem = list.stream().filter(i -> !i.isPrefix()).findFirst().orElse(null);
+
+ assertNotNull(prefixItem);
+ assertEquals("dir/", prefixItem.getName());
+ assertTrue(prefixItem.isPrefix());
+
+ assertNotNull(blobItem);
+ assertEquals("topblob", blobItem.getName());
+ assertFalse(blobItem.isPrefix());
+ assertNotNull(blobItem.getProperties());
+ assertEquals(DATA.getDefaultDataSize(), blobItem.getProperties().getContentLength());
+ assertEquals(BlobType.BLOCK_BLOB, blobItem.getProperties().getBlobType());
+ assertNotNull(blobItem.getProperties().getLastModified());
+ assertNotNull(blobItem.getProperties().getETag());
+ }).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowWithMetadata() {
+ String blobName = generateBlobName();
+ Map metadata = new HashMap<>();
+ metadata.put("testkey", "testvalue");
+
+ Mono> uploads = ccAsync.getBlobAsyncClient("dir/" + blobName)
+ .getBlockBlobAsyncClient()
+ .uploadWithResponse(DATA.getDefaultFlux(), DATA.getDefaultDataSize(), null, metadata, null, null, null)
+ .then(ccAsync.getBlobAsyncClient("topblob")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()));
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setPrefix("dir/")
+ .setDetails(new BlobListDetails().setRetrieveMetadata(true));
+
+ StepVerifier.create(uploads.thenMany(ccAsync.listBlobsByHierarchy("/", options))).assertNext(item -> {
+ assertFalse(item.isPrefix());
+ assertNotNull(item.getMetadata());
+ assertEquals("testvalue", item.getMetadata().get("testkey"));
+ }).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsByHierarchyArrowPagination() {
+ // Upload blobs across multiple directories
+ Flux uploads = Flux.concat(
+ Flux.range(0, 3)
+ .flatMap(i -> ccAsync.getBlobAsyncClient("dir" + i + "/blob")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())),
+ ccAsync.getBlobAsyncClient("topblob")
+ .getBlockBlobAsyncClient()
+ .upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()));
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setMaxResultsPerPage(1);
+
+ Mono> result
+ = uploads.then(ccAsync.listBlobsByHierarchy("/", options).byPage().doOnNext(page -> {
+ assertTrue(page.getValue().size() <= 1);
+ }).flatMap(page -> Flux.fromIterable(page.getValue())).collectList());
+
+ // 3 prefixes + 1 blob = 4 items
+ StepVerifier.create(result).assertNext(allItems -> assertEquals(4, allItems.size())).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06")
+ public void listBlobsArrowWithTags() {
+ // Upload a blob and set tags
+ String blobName = generateBlobName();
+ BlockBlobAsyncClient bc = ccAsync.getBlobAsyncClient(blobName).getBlockBlobAsyncClient();
+
+ Map tags = new HashMap<>();
+ tags.put("tagkey", "tagvalue");
+
+ ListBlobsOptions options
+ = new ListBlobsOptions().setStorageResponseSerializationFormat(StorageResponseSerializationFormat.ARROW)
+ .setDetails(new BlobListDetails().setRetrieveTags(true));
+
+ Mono> upload = bc.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize())
+ .then(ccAsync.getBlobAsyncClient(blobName).setTags(tags));
+
+ StepVerifier.create(upload.thenMany(ccAsync.listBlobs(options, null))).assertNext(item -> {
+ assertEquals(blobName, item.getName());
+ assertNotNull(item.getTags());
+ assertEquals("tagvalue", item.getTags().get("tagkey"));
+ }).verifyComplete();
+ }
}
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/ArrowBlobListDeserializerTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/ArrowBlobListDeserializerTests.java
new file mode 100644
index 000000000000..e23bf2cbfae0
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/ArrowBlobListDeserializerTests.java
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util;
+
+import com.azure.storage.blob.implementation.models.BlobListArrowParseException;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ArrowBlobListDeserializerTests {
+ @Test
+ public void parseNullStreamFailsFast() {
+ BlobListArrowParseException exception
+ = assertThrows(BlobListArrowParseException.class, () -> ArrowBlobListDeserializer.deserialize(null));
+
+ assertTrue(exception.getMessage().startsWith("ListBlobs Arrow parse failure:"));
+ }
+
+ @Test
+ public void parseInvalidPayloadFailsFast() {
+ ByteArrayInputStream invalidPayload
+ = new ByteArrayInputStream("not-an-arrow-stream".getBytes(StandardCharsets.UTF_8));
+
+ BlobListArrowParseException exception = assertThrows(BlobListArrowParseException.class,
+ () -> ArrowBlobListDeserializer.deserialize(invalidPayload));
+
+ assertTrue(exception.getMessage().startsWith("ListBlobs Arrow parse failure:"));
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowAccessorParityTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowAccessorParityTests.java
new file mode 100644
index 000000000000..a59a2e215723
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowAccessorParityTests.java
@@ -0,0 +1,298 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util;
+
+import com.azure.storage.blob.implementation.util.arrow.Buffer;
+import com.azure.storage.blob.implementation.util.arrow.Field;
+import com.azure.storage.blob.implementation.util.arrow.Int;
+import com.azure.storage.blob.implementation.util.arrow.KeyValue;
+import com.azure.storage.blob.implementation.util.arrow.Message;
+import com.azure.storage.blob.implementation.util.arrow.MessageHeader;
+import com.azure.storage.blob.implementation.util.arrow.RecordBatch;
+import com.azure.storage.blob.implementation.util.arrow.Schema;
+import com.azure.storage.blob.implementation.util.arrow.Timestamp;
+import com.azure.storage.blob.implementation.util.arrow.Type;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.BitVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.SmallIntVector;
+import org.apache.arrow.vector.TimeStampSecVector;
+import org.apache.arrow.vector.UInt1Vector;
+import org.apache.arrow.vector.UInt4Vector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.complex.MapVector;
+import org.apache.arrow.vector.complex.impl.UnionMapWriter;
+import org.apache.arrow.vector.ipc.ArrowStreamWriter;
+import org.apache.arrow.vector.util.Text;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Differential drift detector for the vendored Arrow FlatBuffer accessors.
+ *
+ * This test builds a real Arrow IPC stream with the official {@code arrow-vector} writer, then decodes the same bytes
+ * twice: once with Apache Arrow's own generated {@code org.apache.arrow.flatbuf.*} accessors (the upstream source of
+ * truth, available at test scope via {@code arrow-vector} → {@code arrow-format}) and once with the vendored
+ * accessors in {@code com.azure.storage.blob.implementation.util.arrow}. It asserts every accessor the ListBlobs reader
+ * relies on returns identical results from both implementations.
+ *
+ * Why this exists: the vendored accessors hardcode FlatBuffer vtable offsets and struct sizes taken
+ * from a specific Arrow schema revision. When the Arrow test dependency is upgraded (for example to 19.x), re-running
+ * this test re-validates the vendored copy against the new upstream: if Arrow renumbers a field, resizes a struct, or
+ * changes an accessor's semantics in a way that affects how ListBlobs payloads are parsed, this test fails and points
+ * at the exact accessor that diverged, so the vendored copy can be brought back in sync. A compatible upgrade that does
+ * not touch these structures leaves the test passing (no false positives).
+ */
+public class BlobListArrowAccessorParityTests {
+
+ private static final int CONTINUATION_MARKER = 0xFFFFFFFF;
+
+ @Test
+ public void vendoredAccessorsMatchArrowAccessors() throws Exception {
+ byte[] stream;
+ try (BufferAllocator allocator = new RootAllocator()) {
+ stream = buildRepresentativePayload(allocator);
+ }
+
+ List messages = extractMessageMetadata(stream);
+ boolean sawSchema = false;
+ boolean sawRecordBatch = false;
+
+ for (byte[] metadata : messages) {
+ org.apache.arrow.flatbuf.Message arrowMessage
+ = org.apache.arrow.flatbuf.Message.getRootAsMessage(littleEndian(metadata));
+ Message vendoredMessage = Message.getRootAsMessage(littleEndian(metadata));
+
+ assertEquals(arrowMessage.headerType(), vendoredMessage.headerType(), "headerType");
+ assertEquals(arrowMessage.bodyLength(), vendoredMessage.bodyLength(), "bodyLength");
+
+ if (vendoredMessage.headerType() == MessageHeader.SCHEMA) {
+ sawSchema = true;
+ org.apache.arrow.flatbuf.Schema arrowSchema
+ = (org.apache.arrow.flatbuf.Schema) arrowMessage.header(new org.apache.arrow.flatbuf.Schema());
+ Schema vendoredSchema = (Schema) vendoredMessage.header(new Schema());
+ assertNotNull(arrowSchema, "arrow schema header");
+ assertNotNull(vendoredSchema, "vendored schema header");
+ compareSchema(arrowSchema, vendoredSchema);
+ } else if (vendoredMessage.headerType() == MessageHeader.RECORD_BATCH) {
+ sawRecordBatch = true;
+ org.apache.arrow.flatbuf.RecordBatch arrowBatch = (org.apache.arrow.flatbuf.RecordBatch) arrowMessage
+ .header(new org.apache.arrow.flatbuf.RecordBatch());
+ RecordBatch vendoredBatch = (RecordBatch) vendoredMessage.header(new RecordBatch());
+ assertNotNull(arrowBatch, "arrow record batch header");
+ assertNotNull(vendoredBatch, "vendored record batch header");
+ compareRecordBatch(arrowBatch, vendoredBatch);
+ }
+ }
+
+ assertTrue(sawSchema, "expected a Schema message in the stream");
+ assertTrue(sawRecordBatch, "expected a RecordBatch message in the stream");
+ }
+
+ private static void compareSchema(org.apache.arrow.flatbuf.Schema arrow, Schema vendored) {
+ assertEquals(arrow.endianness(), vendored.endianness(), "schema.endianness");
+ assertEquals(arrow.fieldsLength(), vendored.fieldsLength(), "schema.fieldsLength");
+ for (int i = 0; i < arrow.fieldsLength(); i++) {
+ compareField(arrow.fields(i), vendored.fields(i));
+ }
+ assertEquals(arrow.customMetadataLength(), vendored.customMetadataLength(), "schema.customMetadataLength");
+ for (int i = 0; i < arrow.customMetadataLength(); i++) {
+ org.apache.arrow.flatbuf.KeyValue arrowKv = arrow.customMetadata(i);
+ KeyValue vendoredKv = vendored.customMetadata(i);
+ assertEquals(arrowKv.key(), vendoredKv.key(), "keyValue.key");
+ assertEquals(arrowKv.value(), vendoredKv.value(), "keyValue.value");
+ }
+ }
+
+ private static void compareField(org.apache.arrow.flatbuf.Field arrow, Field vendored) {
+ assertEquals(arrow.name(), vendored.name(), "field.name");
+ assertEquals(arrow.typeType(), vendored.typeType(), "field.typeType for " + vendored.name());
+ assertEquals(arrow.childrenLength(), vendored.childrenLength(), "field.childrenLength for " + vendored.name());
+
+ if (vendored.typeType() == Type.INT) {
+ org.apache.arrow.flatbuf.Int arrowInt
+ = (org.apache.arrow.flatbuf.Int) arrow.type(new org.apache.arrow.flatbuf.Int());
+ Int vendoredInt = (Int) vendored.type(new Int());
+ assertNotNull(arrowInt, "arrow Int type for " + vendored.name());
+ assertNotNull(vendoredInt, "vendored Int type for " + vendored.name());
+ assertEquals(arrowInt.bitWidth(), vendoredInt.bitWidth(), "int.bitWidth for " + vendored.name());
+ assertEquals(arrowInt.isSigned(), vendoredInt.isSigned(), "int.isSigned for " + vendored.name());
+ } else if (vendored.typeType() == Type.TIMESTAMP) {
+ org.apache.arrow.flatbuf.Timestamp arrowTs
+ = (org.apache.arrow.flatbuf.Timestamp) arrow.type(new org.apache.arrow.flatbuf.Timestamp());
+ Timestamp vendoredTs = (Timestamp) vendored.type(new Timestamp());
+ assertNotNull(arrowTs, "arrow Timestamp type for " + vendored.name());
+ assertNotNull(vendoredTs, "vendored Timestamp type for " + vendored.name());
+ assertEquals(arrowTs.unit(), vendoredTs.unit(), "timestamp.unit for " + vendored.name());
+ }
+
+ for (int i = 0; i < arrow.childrenLength(); i++) {
+ compareField(arrow.children(i), vendored.children(i));
+ }
+ }
+
+ private static void compareRecordBatch(org.apache.arrow.flatbuf.RecordBatch arrow, RecordBatch vendored) {
+ assertEquals(arrow.length(), vendored.length(), "recordBatch.length");
+
+ assertEquals(arrow.nodesLength(), vendored.nodesLength(), "recordBatch.nodesLength");
+ for (int i = 0; i < arrow.nodesLength(); i++) {
+ assertEquals(arrow.nodes(i).length(), vendored.nodes(i).length(), "fieldNode.length at " + i);
+ }
+
+ assertEquals(arrow.buffersLength(), vendored.buffersLength(), "recordBatch.buffersLength");
+ for (int i = 0; i < arrow.buffersLength(); i++) {
+ Buffer vendoredBuffer = vendored.buffers(i);
+ assertEquals(arrow.buffers(i).offset(), vendoredBuffer.offset(), "buffer.offset at " + i);
+ assertEquals(arrow.buffers(i).length(), vendoredBuffer.length(), "buffer.length at " + i);
+ }
+
+ assertEquals(arrow.compression() == null, vendored.compression() == null, "recordBatch.compression presence");
+ }
+
+ // region helpers
+
+ private static ByteBuffer littleEndian(byte[] metadata) {
+ return ByteBuffer.wrap(metadata).order(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ /**
+ * Splits an Arrow IPC stream into the metadata FlatBuffer of each encapsulated message, mirroring the framing the
+ * production reader performs (continuation marker + metadata length prefix, body skipped).
+ */
+ private static List extractMessageMetadata(byte[] stream) {
+ List messages = new ArrayList<>();
+ ByteBuffer buffer = ByteBuffer.wrap(stream).order(ByteOrder.LITTLE_ENDIAN);
+ int pos = 0;
+ int length = stream.length;
+ while (pos + 4 <= length) {
+ int marker = buffer.getInt(pos);
+ pos += 4;
+
+ int metadataLength;
+ if (marker == CONTINUATION_MARKER) {
+ if (pos + 4 > length) {
+ break;
+ }
+ metadataLength = buffer.getInt(pos);
+ pos += 4;
+ } else {
+ metadataLength = marker;
+ }
+
+ if (metadataLength == 0) {
+ break;
+ }
+
+ byte[] metadata = new byte[metadataLength];
+ System.arraycopy(stream, pos, metadata, 0, metadataLength);
+ messages.add(metadata);
+ pos += metadataLength;
+
+ long bodyLength = Message.getRootAsMessage(littleEndian(metadata)).bodyLength();
+ pos += (int) bodyLength;
+ }
+ return messages;
+ }
+
+ /**
+ * Builds an Arrow IPC stream with a representative ListBlobs schema (string; signed and unsigned integers of
+ * several bit widths; boolean; second-precision timestamp; and a map<string,string> column) plus schema-level
+ * metadata, so the differential comparison exercises every vendored accessor — including {@code Int.bitWidth}
+ * / {@code Int.isSigned} across multiple widths and both signedness values, and the nested map → struct →
+ * key/value fields.
+ */
+ private static byte[] buildRepresentativePayload(BufferAllocator allocator) throws Exception {
+ VarCharVector name = new VarCharVector("Name", allocator);
+ UInt1Vector uint8 = new UInt1Vector("U8", allocator);
+ SmallIntVector int16 = new SmallIntVector("I16", allocator);
+ UInt4Vector uint32 = new UInt4Vector("U32", allocator);
+ BigIntVector contentLength = new BigIntVector("Content-Length", allocator);
+ BitVector deleted = new BitVector("Deleted", allocator);
+ TimeStampSecVector creationTime = new TimeStampSecVector("Creation-Time", allocator);
+ MapVector metadata = MapVector.empty("Metadata", allocator, false);
+
+ name.allocateNew();
+ uint8.allocateNew();
+ int16.allocateNew();
+ uint32.allocateNew();
+ contentLength.allocateNew();
+ deleted.allocateNew();
+ creationTime.allocateNew();
+
+ name.setSafe(0, "blob1".getBytes(StandardCharsets.UTF_8));
+ uint8.setSafe(0, 200);
+ int16.setSafe(0, -5);
+ uint32.setSafe(0, 42);
+ contentLength.setSafe(0, 7L);
+ deleted.setSafe(0, 0);
+ creationTime.setSafe(0, 1000L);
+
+ name.setValueCount(1);
+ uint8.setValueCount(1);
+ int16.setValueCount(1);
+ uint32.setValueCount(1);
+ contentLength.setValueCount(1);
+ deleted.setValueCount(1);
+ creationTime.setValueCount(1);
+
+ UnionMapWriter mapWriter = metadata.getWriter();
+ mapWriter.setPosition(0);
+ mapWriter.startMap();
+ mapWriter.startEntry();
+ mapWriter.key().varChar().writeVarChar(new Text("k1"));
+ mapWriter.value().varChar().writeVarChar(new Text("v1"));
+ mapWriter.endEntry();
+ mapWriter.endMap();
+ metadata.setValueCount(1);
+
+ List vectors = new ArrayList<>();
+ vectors.add(name);
+ vectors.add(uint8);
+ vectors.add(int16);
+ vectors.add(uint32);
+ vectors.add(contentLength);
+ vectors.add(deleted);
+ vectors.add(creationTime);
+ vectors.add(metadata);
+
+ List fields = new ArrayList<>();
+ for (FieldVector vector : vectors) {
+ fields.add(vector.getField());
+ }
+
+ Map schemaMetadata = new LinkedHashMap<>();
+ schemaMetadata.put("NextMarker", "nextPage");
+ schemaMetadata.put("NumberOfRecords", "1");
+ org.apache.arrow.vector.types.pojo.Schema schema
+ = new org.apache.arrow.vector.types.pojo.Schema(fields, schemaMetadata);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (VectorSchemaRoot root = new VectorSchemaRoot(schema, vectors, 1);
+ ArrowStreamWriter writer = new ArrowStreamWriter(root, null, out)) {
+ writer.start();
+ writer.writeBatch();
+ writer.end();
+ }
+ return out.toByteArray();
+ }
+
+ //endregion
+}
+
+
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowFlatbufConstantsTest.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowFlatbufConstantsTest.java
new file mode 100644
index 000000000000..3ed329140545
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowFlatbufConstantsTest.java
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util;
+
+import com.azure.storage.blob.implementation.util.arrow.Endianness;
+import com.azure.storage.blob.implementation.util.arrow.MessageHeader;
+import com.azure.storage.blob.implementation.util.arrow.TimeUnit;
+import com.azure.storage.blob.implementation.util.arrow.Type;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Fidelity tests that pin the vendored Arrow FlatBuffer metadata constants
+ * (in {@code com.azure.storage.blob.implementation.util.arrow}) to the values defined by the official
+ * {@code org.apache.arrow.flatbuf} classes shipped in {@code arrow-format} (pulled in transitively at test scope via
+ * {@code arrow-vector}).
+ *
+ * {@link BlobListArrowStreamReaderTests} validates decoding behavior against payloads written by the real
+ * Arrow writer, but only exercises the enum values that a representative ListBlobs response happens to use. These tests
+ * close that gap by asserting every enum ordinal and name our reader relies on matches Apache Arrow exactly,
+ * including values that live on error/rejection paths (for example dictionary batches). If a future {@code arrow}
+ * version bump renumbers or extends one of these enums, these tests fail loudly so the vendored copy can be reviewed.
+ */
+public class BlobListArrowFlatbufConstantsTest {
+
+ @Test
+ public void messageHeaderUnionMatchesArrow() {
+ assertEquals(org.apache.arrow.flatbuf.MessageHeader.NONE, MessageHeader.NONE);
+ assertEquals(org.apache.arrow.flatbuf.MessageHeader.Schema, MessageHeader.SCHEMA);
+ assertEquals(org.apache.arrow.flatbuf.MessageHeader.DictionaryBatch, MessageHeader.DICTIONARY_BATCH);
+ assertEquals(org.apache.arrow.flatbuf.MessageHeader.RecordBatch, MessageHeader.RECORD_BATCH);
+ assertEquals(org.apache.arrow.flatbuf.MessageHeader.Tensor, MessageHeader.TENSOR);
+ assertEquals(org.apache.arrow.flatbuf.MessageHeader.SparseTensor, MessageHeader.SPARSE_TENSOR);
+ }
+
+ @Test
+ public void endiannessMatchesArrow() {
+ assertEquals(org.apache.arrow.flatbuf.Endianness.Little, Endianness.LITTLE);
+ assertEquals(org.apache.arrow.flatbuf.Endianness.Big, Endianness.BIG);
+ }
+
+ @Test
+ public void timeUnitMatchesArrow() {
+ assertEquals(org.apache.arrow.flatbuf.TimeUnit.SECOND, TimeUnit.SECOND);
+ assertEquals(org.apache.arrow.flatbuf.TimeUnit.MILLISECOND, TimeUnit.MILLISECOND);
+ assertEquals(org.apache.arrow.flatbuf.TimeUnit.MICROSECOND, TimeUnit.MICROSECOND);
+ assertEquals(org.apache.arrow.flatbuf.TimeUnit.NANOSECOND, TimeUnit.NANOSECOND);
+
+ // The vendored name table must match Arrow's, element-for-element and in length.
+ assertEquals(org.apache.arrow.flatbuf.TimeUnit.names.length, namesLength(TimeUnit::name),
+ "Arrow TimeUnit enum changed size; review the vendored TimeUnit.");
+ for (int i = 0; i < org.apache.arrow.flatbuf.TimeUnit.names.length; i++) {
+ assertEquals(org.apache.arrow.flatbuf.TimeUnit.name(i), TimeUnit.name(i),
+ "TimeUnit name mismatch at ordinal " + i);
+ }
+ }
+
+ @Test
+ public void typeUnionOrdinalsMatchArrow() {
+ assertEquals(org.apache.arrow.flatbuf.Type.NONE, Type.NONE);
+ assertEquals(org.apache.arrow.flatbuf.Type.Null, Type.NULL);
+ assertEquals(org.apache.arrow.flatbuf.Type.Int, Type.INT);
+ assertEquals(org.apache.arrow.flatbuf.Type.FloatingPoint, Type.FLOATING_POINT);
+ assertEquals(org.apache.arrow.flatbuf.Type.Binary, Type.BINARY);
+ assertEquals(org.apache.arrow.flatbuf.Type.Utf8, Type.UTF8);
+ assertEquals(org.apache.arrow.flatbuf.Type.Bool, Type.BOOL);
+ assertEquals(org.apache.arrow.flatbuf.Type.Decimal, Type.DECIMAL);
+ assertEquals(org.apache.arrow.flatbuf.Type.Date, Type.DATE);
+ assertEquals(org.apache.arrow.flatbuf.Type.Time, Type.TIME);
+ assertEquals(org.apache.arrow.flatbuf.Type.Timestamp, Type.TIMESTAMP);
+ assertEquals(org.apache.arrow.flatbuf.Type.Interval, Type.INTERVAL);
+ assertEquals(org.apache.arrow.flatbuf.Type.List, Type.LIST);
+ assertEquals(org.apache.arrow.flatbuf.Type.Struct_, Type.STRUCT);
+ assertEquals(org.apache.arrow.flatbuf.Type.Union, Type.UNION);
+ assertEquals(org.apache.arrow.flatbuf.Type.FixedSizeBinary, Type.FIXED_SIZE_BINARY);
+ assertEquals(org.apache.arrow.flatbuf.Type.FixedSizeList, Type.FIXED_SIZE_LIST);
+ assertEquals(org.apache.arrow.flatbuf.Type.Map, Type.MAP);
+ assertEquals(org.apache.arrow.flatbuf.Type.Duration, Type.DURATION);
+ assertEquals(org.apache.arrow.flatbuf.Type.LargeBinary, Type.LARGE_BINARY);
+ assertEquals(org.apache.arrow.flatbuf.Type.LargeUtf8, Type.LARGE_UTF8);
+ assertEquals(org.apache.arrow.flatbuf.Type.LargeList, Type.LARGE_LIST);
+ assertEquals(org.apache.arrow.flatbuf.Type.RunEndEncoded, Type.RUN_END_ENCODED);
+ assertEquals(org.apache.arrow.flatbuf.Type.BinaryView, Type.BINARY_VIEW);
+ assertEquals(org.apache.arrow.flatbuf.Type.Utf8View, Type.UTF8_VIEW);
+ assertEquals(org.apache.arrow.flatbuf.Type.ListView, Type.LIST_VIEW);
+ assertEquals(org.apache.arrow.flatbuf.Type.LargeListView, Type.LARGE_LIST_VIEW);
+ }
+
+ @Test
+ public void typeUnionNamesMatchArrow() {
+ // A length mismatch means Arrow added/removed a Type; the vendored Type (and reader's switch) must be reviewed.
+ assertEquals(org.apache.arrow.flatbuf.Type.names.length, namesLength(Type::name),
+ "Arrow Type enum changed size; review the vendored Type and the reader's type switch.");
+ for (int i = 0; i < org.apache.arrow.flatbuf.Type.names.length; i++) {
+ assertEquals(org.apache.arrow.flatbuf.Type.name(i), Type.name(i), "Type name mismatch at ordinal " + i);
+ }
+ }
+
+ /** Counts a vendored name table's length by probing for its array bound. */
+ private static int namesLength(java.util.function.IntFunction nameFn) {
+ int count = 0;
+ while (true) {
+ try {
+ nameFn.apply(count);
+ count++;
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return count;
+ }
+ }
+ }
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderRejectionTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderRejectionTests.java
new file mode 100644
index 000000000000..f91a9e56ab50
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderRejectionTests.java
@@ -0,0 +1,182 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util;
+
+import com.azure.storage.blob.implementation.models.BlobListArrowParseException;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.Float8Vector;
+import org.apache.arrow.vector.TimeStampMilliVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.complex.MapVector;
+import org.apache.arrow.vector.complex.impl.UnionMapWriter;
+import org.apache.arrow.vector.dictionary.Dictionary;
+import org.apache.arrow.vector.dictionary.DictionaryEncoder;
+import org.apache.arrow.vector.dictionary.DictionaryProvider;
+import org.apache.arrow.vector.ipc.ArrowStreamWriter;
+import org.apache.arrow.vector.types.pojo.DictionaryEncoding;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.util.Text;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Parity tests for Arrow IPC content the ListBlobs reader intentionally rejects or treats as an error. Where possible
+ * the payloads are produced with the official {@code arrow-vector} writer (test scope) so the vendored reader's
+ * rejection paths are validated against genuine Arrow output rather than hand-crafted bytes. The remaining cases cover
+ * malformed/edge inputs that the writer cannot produce (null and empty streams).
+ *
+ * Behavioral parity for supported content lives in {@link BlobListArrowStreamReaderTests}; enum/ordinal
+ * fidelity lives in {@link BlobListArrowFlatbufConstantsTest}.
+ */
+public class BlobListArrowStreamReaderRejectionTests {
+
+ @Test
+ public void rejectsNullStream() {
+ BlobListArrowParseException ex = assertThrows(BlobListArrowParseException.class,
+ () -> ArrowBlobListDeserializer.deserialize(null));
+ assertTrue(ex.getMessage().contains("input stream is null"), "Unexpected message: " + ex.getMessage());
+ }
+
+ @Test
+ public void rejectsStreamWithNoSchema() {
+ assertRejected(new byte[0], "stream contained no schema");
+ }
+
+ @Test
+ public void rejectsDictionaryEncodedStreams() throws Exception {
+ byte[] payload;
+ try (BufferAllocator allocator = new RootAllocator()) {
+ payload = buildDictionaryEncodedPayload(allocator);
+ }
+ assertRejected(payload, "dictionary-encoded streams are not supported");
+ }
+
+ @Test
+ public void rejectsUnsupportedColumnType() throws Exception {
+ byte[] payload;
+ try (BufferAllocator allocator = new RootAllocator()) {
+ Float8Vector score = new Float8Vector("Score", allocator);
+ score.allocateNew();
+ score.setSafe(0, 1.5);
+ score.setValueCount(1);
+ payload = writeBatch(allocator, Collections.singletonList(score), 1);
+ }
+ assertRejected(payload, "unsupported Arrow type 'FloatingPoint'");
+ }
+
+ @Test
+ public void rejectsUnsupportedTimestampUnit() throws Exception {
+ byte[] payload;
+ try (BufferAllocator allocator = new RootAllocator()) {
+ TimeStampMilliVector creationTime = new TimeStampMilliVector("Creation-Time", allocator);
+ creationTime.allocateNew();
+ creationTime.setSafe(0, 1000L);
+ creationTime.setValueCount(1);
+ payload = writeBatch(allocator, Collections.singletonList(creationTime), 1);
+ }
+ assertRejected(payload, "unsupported timestamp unit 'MILLISECOND'");
+ }
+
+ @Test
+ public void rejectsMapWithNonStringValues() throws Exception {
+ byte[] payload;
+ try (BufferAllocator allocator = new RootAllocator()) {
+ MapVector metadata = MapVector.empty("Metadata", allocator, false);
+ UnionMapWriter mapWriter = metadata.getWriter();
+ mapWriter.setPosition(0);
+ mapWriter.startMap();
+ mapWriter.startEntry();
+ mapWriter.key().varChar().writeVarChar(new Text("k1"));
+ mapWriter.value().bigInt().writeBigInt(42L);
+ mapWriter.endEntry();
+ mapWriter.endMap();
+ metadata.setValueCount(1);
+ payload = writeBatch(allocator, Collections.singletonList(metadata), 1);
+ }
+ assertRejected(payload, "map entries must be string keys and values");
+ }
+
+ // region helpers
+
+ private static void assertRejected(byte[] payload, String expectedMessageFragment) {
+ InputStream stream = new ByteArrayInputStream(payload);
+ BlobListArrowParseException ex
+ = assertThrows(BlobListArrowParseException.class, () -> ArrowBlobListDeserializer.deserialize(stream));
+ assertTrue(ex.getMessage().contains(expectedMessageFragment), "Unexpected message: " + ex.getMessage());
+ }
+
+ /**
+ * Writes a single-batch Arrow IPC stream from the supplied vectors. The vectors are owned by (and closed with) the
+ * returned {@link VectorSchemaRoot}.
+ */
+ private static byte[] writeBatch(BufferAllocator allocator, List vectors, int rowCount)
+ throws Exception {
+ List fields = new ArrayList<>(vectors.size());
+ for (FieldVector vector : vectors) {
+ fields.add(vector.getField());
+ }
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (VectorSchemaRoot root = new VectorSchemaRoot(fields, vectors, rowCount);
+ ArrowStreamWriter writer = new ArrowStreamWriter(root, null, out)) {
+ writer.start();
+ writer.writeBatch();
+ writer.end();
+ }
+ return out.toByteArray();
+ }
+
+ /**
+ * Builds an Arrow IPC stream whose single column is dictionary-encoded, which forces the official writer to emit a
+ * {@code DictionaryBatch} message ahead of the record batch.
+ */
+ private static byte[] buildDictionaryEncodedPayload(BufferAllocator allocator) throws Exception {
+ VarCharVector dictVector = new VarCharVector("Name-dict", allocator);
+ VarCharVector name = new VarCharVector("Name", allocator);
+ try {
+ dictVector.allocateNew();
+ dictVector.setSafe(0, "blob1".getBytes(StandardCharsets.UTF_8));
+ dictVector.setSafe(1, "blob2".getBytes(StandardCharsets.UTF_8));
+ dictVector.setValueCount(2);
+ Dictionary dictionary = new Dictionary(dictVector, new DictionaryEncoding(1L, false, null));
+
+ name.allocateNew();
+ name.setSafe(0, "blob1".getBytes(StandardCharsets.UTF_8));
+ name.setSafe(1, "blob2".getBytes(StandardCharsets.UTF_8));
+ name.setValueCount(2);
+
+ DictionaryProvider.MapDictionaryProvider provider = new DictionaryProvider.MapDictionaryProvider();
+ provider.put(dictionary);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ FieldVector encoded = (FieldVector) DictionaryEncoder.encode(name, dictionary);
+ try (VectorSchemaRoot root = new VectorSchemaRoot(Collections.singletonList(encoded.getField()),
+ Collections.singletonList(encoded), 2);
+ ArrowStreamWriter writer = new ArrowStreamWriter(root, provider, out)) {
+ writer.start();
+ writer.writeBatch();
+ writer.end();
+ }
+ return out.toByteArray();
+ } finally {
+ name.close();
+ dictVector.close();
+ }
+ }
+
+ //endregion
+}
+
diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderTests.java
new file mode 100644
index 000000000000..b03027eeb841
--- /dev/null
+++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/BlobListArrowStreamReaderTests.java
@@ -0,0 +1,184 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.storage.blob.implementation.util;
+
+import com.azure.storage.blob.implementation.models.BlobItemInternal;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.BitVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.TimeStampSecVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.complex.MapVector;
+import org.apache.arrow.vector.complex.impl.UnionMapWriter;
+import org.apache.arrow.vector.ipc.ArrowStreamWriter;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.arrow.vector.util.Text;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Parity tests that build a real Arrow IPC payload with the official {@code arrow-vector} writer and validate that the
+ * internal {@link BlobListArrowStreamReader} / {@link ArrowBlobListDeserializer} decode it identically. This proves the
+ * custom reader has the same implementation as the Apache Arrow parser
+ */
+public class BlobListArrowStreamReaderTests {
+
+ @Test
+ public void parsesRealArrowPayload() throws Exception {
+ byte[] payload;
+ try (BufferAllocator allocator = new RootAllocator()) {
+ payload = buildPayload(allocator);
+ }
+
+ ArrowBlobListDeserializer.ArrowListBlobsResult result
+ = ArrowBlobListDeserializer.deserialize(new ByteArrayInputStream(payload));
+
+ // Schema metadata
+ assertEquals("nextPage", result.getNextMarker());
+ assertEquals(Integer.valueOf(2), result.getNumberOfRecords());
+
+ // Two rows: one blob, one prefix
+ List items = result.getBlobItems();
+ assertEquals(2, items.size());
+
+ BlobItemInternal blob = items.get(0);
+ assertNotNull(blob.getName());
+ assertEquals("blob1", blob.getName().getContent());
+ assertNull(blob.isPrefix());
+ assertEquals(Boolean.FALSE, blob.isDeleted());
+ assertNotNull(blob.getProperties());
+ assertEquals(7L, (long) blob.getProperties().getContentLength());
+ assertEquals("application/octet-stream", blob.getProperties().getContentType());
+ assertNotNull(blob.getProperties().getCreationTime());
+ assertEquals(1000L, blob.getProperties().getCreationTime().toEpochSecond());
+
+ Map metadata = blob.getMetadata();
+ assertNotNull(metadata);
+ assertEquals("v1", metadata.get("k1"));
+ assertEquals("v2", metadata.get("k2"));
+
+ BlobItemInternal prefix = items.get(1);
+ assertNotNull(prefix.getName());
+ assertEquals("dir/", prefix.getName().getContent());
+ assertTrue(prefix.isPrefix());
+ }
+
+ @Test
+ public void parsesEmptyMetadataAsNull() throws Exception {
+ byte[] payload;
+ try (BufferAllocator allocator = new RootAllocator()) {
+ payload = buildPayload(allocator);
+ }
+
+ ArrowBlobListDeserializer.ArrowListBlobsResult result
+ = ArrowBlobListDeserializer.deserialize(new ByteArrayInputStream(payload));
+ // Row 1 (prefix) had no metadata entries; ensure prefix path doesn't surface an (empty) metadata map.
+ assertNull(result.getBlobItems().get(1).getMetadata());
+ assertFalse(result.getBlobItems().isEmpty());
+ }
+
+ /**
+ * Builds an Arrow IPC stream with a representative ListBlobs schema: string, integer, boolean, second-precision
+ * timestamp, content-type string and a map<string,string> metadata column, plus schema-level NextMarker and
+ * NumberOfRecords metadata.
+ */
+ private static byte[] buildPayload(BufferAllocator allocator) throws Exception {
+ VarCharVector name = new VarCharVector("Name", allocator);
+ VarCharVector resourceType = new VarCharVector("ResourceType", allocator);
+ BigIntVector contentLength = new BigIntVector("Content-Length", allocator);
+ VarCharVector contentType = new VarCharVector("Content-Type", allocator);
+ BitVector deleted = new BitVector("Deleted", allocator);
+ TimeStampSecVector creationTime = new TimeStampSecVector("Creation-Time", allocator);
+ MapVector metadata = MapVector.empty("Metadata", allocator, false);
+
+ name.allocateNew();
+ resourceType.allocateNew();
+ contentLength.allocateNew();
+ contentType.allocateNew();
+ deleted.allocateNew();
+ creationTime.allocateNew();
+
+ // Row 0: a real blob.
+ name.setSafe(0, "blob1".getBytes(StandardCharsets.UTF_8));
+ // resourceType[0] left null -> not a prefix.
+ contentLength.setSafe(0, 7L);
+ contentType.setSafe(0, "application/octet-stream".getBytes(StandardCharsets.UTF_8));
+ deleted.setSafe(0, 0);
+ creationTime.setSafe(0, 1000L);
+
+ // Row 1: a virtual directory (prefix).
+ name.setSafe(1, "dir/".getBytes(StandardCharsets.UTF_8));
+ resourceType.setSafe(1, "blobprefix".getBytes(StandardCharsets.UTF_8));
+ // remaining columns null for the prefix row.
+
+ name.setValueCount(2);
+ resourceType.setValueCount(2);
+ contentLength.setValueCount(2);
+ contentType.setValueCount(2);
+ deleted.setValueCount(2);
+ creationTime.setValueCount(2);
+
+ UnionMapWriter mapWriter = metadata.getWriter();
+ mapWriter.setPosition(0);
+ mapWriter.startMap();
+ writeEntry(mapWriter, "k1", "v1");
+ writeEntry(mapWriter, "k2", "v2");
+ mapWriter.endMap();
+ // Row 1 metadata left null.
+ metadata.setValueCount(2);
+
+ List vectors = new ArrayList<>();
+ vectors.add(name);
+ vectors.add(resourceType);
+ vectors.add(contentLength);
+ vectors.add(contentType);
+ vectors.add(deleted);
+ vectors.add(creationTime);
+ vectors.add(metadata);
+
+ List fields = new ArrayList<>();
+ for (FieldVector vector : vectors) {
+ fields.add(vector.getField());
+ }
+
+ Map schemaMetadata = new LinkedHashMap<>();
+ schemaMetadata.put("NextMarker", "nextPage");
+ schemaMetadata.put("NumberOfRecords", "2");
+ Schema schema = new Schema(fields, schemaMetadata);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (VectorSchemaRoot root = new VectorSchemaRoot(schema, vectors, 2);
+ ArrowStreamWriter writer = new ArrowStreamWriter(root, null, out)) {
+ writer.start();
+ writer.writeBatch();
+ writer.end();
+ }
+
+ return out.toByteArray();
+ }
+
+ private static void writeEntry(UnionMapWriter mapWriter, String key, String value) {
+ mapWriter.startEntry();
+ mapWriter.key().varChar().writeVarChar(new Text(key));
+ mapWriter.value().varChar().writeVarChar(new Text(value));
+ mapWriter.endEntry();
+ }
+}
diff --git a/sdk/storage/azure-storage-blob/swagger/README.md b/sdk/storage/azure-storage-blob/swagger/README.md
index 98afe0c616dc..0019d026a0d0 100644
--- a/sdk/storage/azure-storage-blob/swagger/README.md
+++ b/sdk/storage/azure-storage-blob/swagger/README.md
@@ -16,7 +16,7 @@ autorest
### Code generation settings
``` yaml
use: '@autorest/java@4.1.63'
-input-file: https://raw.githubusercontent.com/seanmcc-msft/azure-rest-api-specs/eb29a830edf5db50758e7d044160c7f18077f7f7/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-10-06/blob.json
+input-file: https://raw.githubusercontent.com/nickliu-msft/azure-rest-api-specs/f85584d452061985a5fc21a67b8fc0b46b75188a/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-10-06/blob.json
java: true
output-folder: ../
namespace: com.azure.storage.blob
@@ -591,6 +591,24 @@ directive:
delete $["x-ms-pageable"];
```
+### Delete Container_ListBlobFlatSegment_ApacheArrow x-ms-pageable as response is raw Arrow stream
+``` yaml
+directive:
+- from: swagger-document
+ where: $["x-ms-paths"]["/{containerName}?restype=container&comp=list&flat&arrow"].get
+ transform: >
+ delete $["x-ms-pageable"];
+```
+
+### Delete Container_ListBlobHierarchySegment_ApacheArrow x-ms-pageable as response is raw Arrow stream
+``` yaml
+directive:
+- from: swagger-document
+ where: $["x-ms-paths"]["/{containerName}?restype=container&comp=list&hierarchy&arrow"].get
+ transform: >
+ delete $["x-ms-pageable"];
+```
+
### BlobDeleteType expandable string enum
``` yaml
directive:
@@ -708,4 +726,3 @@ directive:
];
```
-
diff --git a/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/TestEnvironment.java b/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/TestEnvironment.java
index 5d9bc1c9dfac..6134a8dbf9ee 100644
--- a/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/TestEnvironment.java
+++ b/sdk/storage/azure-storage-common/src/test-shared/java/com/azure/storage/common/test/shared/TestEnvironment.java
@@ -107,8 +107,8 @@ private static TestAccount readTestAccountFromEnvironment(String prefix, TestMod
+ "AccountKey=%s;EndpointSuffix=core.windows.net", name, key);
}
}
- String blobEndpoint = String.format(SCHEME + "://%s.blob.core.windows.net", name);
- String blobEndpointSecondary = String.format(SCHEME + "://%s-secondary.blob.core.windows.net", name);
+ String blobEndpoint = String.format(SCHEME + "://%s." + "blob."+ "preprod." +"core.windows.net", name);
+ String blobEndpointSecondary = String.format(SCHEME + "://%s-secondary." + "preprod." +"core.windows.net", name);
String dataLakeEndpoint = String.format(SCHEME + "://%s.dfs.core.windows.net", name);
String queueEndpoint = String.format(SCHEME + "://%s.queue.core.windows.net", name);
String fileEndpoint = String.format(SCHEME + "://%s.file.core.windows.net", name);