2121import org .lance .spark .LanceRuntime ;
2222import org .lance .spark .LanceSparkReadOptions ;
2323import org .lance .spark .read .LanceInputPartition ;
24+ import org .lance .spark .utils .BlobUtils ;
2425import org .lance .spark .utils .Utils ;
2526
2627import org .apache .arrow .vector .ipc .ArrowReader ;
2930
3031import java .io .IOException ;
3132import java .util .Arrays ;
33+ import java .util .HashSet ;
3234import java .util .List ;
35+ import java .util .Set ;
3336import java .util .stream .Collectors ;
3437
3538public class LanceFragmentScanner implements AutoCloseable {
3639 private final Dataset dataset ;
3740 private final LanceScanner scanner ;
3841 private final int fragmentId ;
39- private final boolean withFragemtId ;
42+ private final boolean withFragmentId ;
4043 private final LanceInputPartition inputPartition ;
4144 private final long datasetOpenTimeNs ;
4245 private final long scannerCreateTimeNs ;
4346
47+ /**
48+ * Whether the scanner requested _rowaddr for blob reference support. When true, the _rowaddr
49+ * column in the Arrow batch was implicitly added and should be stripped from user-visible output.
50+ */
51+ private final boolean withRowAddrForBlobs ;
52+
53+ /** The names of blob columns in the projected schema. */
54+ private final Set <String > blobColumnNames ;
55+
4456 private LanceFragmentScanner (
4557 Dataset dataset ,
4658 LanceScanner scanner ,
4759 int fragmentId ,
4860 boolean withFragmentId ,
4961 LanceInputPartition inputPartition ,
5062 long datasetOpenTimeNs ,
51- long scannerCreateTimeNs ) {
63+ long scannerCreateTimeNs ,
64+ boolean withRowAddrForBlobs ,
65+ Set <String > blobColumnNames ) {
5266 this .dataset = dataset ;
5367 this .scanner = scanner ;
5468 this .fragmentId = fragmentId ;
55- this .withFragemtId = withFragmentId ;
69+ this .withFragmentId = withFragmentId ;
5670 this .inputPartition = inputPartition ;
5771 this .datasetOpenTimeNs = datasetOpenTimeNs ;
5872 this .scannerCreateTimeNs = scannerCreateTimeNs ;
73+ this .withRowAddrForBlobs = withRowAddrForBlobs ;
74+ this .blobColumnNames = blobColumnNames ;
5975 }
6076
6177 public static LanceFragmentScanner create (int fragmentId , LanceInputPartition inputPartition ) {
6278 Dataset dataset = null ;
6379 LanceScanner lanceScanner = null ;
6480 try {
6581 LanceSparkReadOptions readOptions = inputPartition .getReadOptions ();
66- // Optionally rebuild the namespace client on the executor so the dataset open routes through
67- // Utils.OpenDatasetBuilder's namespaceClient branch. This preserves the storage options
68- // provider on the Rust side, which refreshes short-lived vended credentials (e.g. STS
69- // tokens) during long-running scans. The price is an eager describeTable() RPC against the
70- // namespace on every fragment open.
71- //
72- // For catalogs whose backing service authenticates per-call (e.g. Hive Metastore over
73- // Kerberos) executors typically lack a TGT and that RPC fails with "GSS initiate failed".
74- // Setting LanceSparkReadOptions.CONFIG_EXECUTOR_CREDENTIAL_REFRESH=false makes executors
75- // skip the rebuild and open the dataset by URI using the initialStorageOptions the driver
76- // already obtained, at the cost of losing the Rust-side credential refresh callback.
7782 if (inputPartition .getNamespaceImpl () != null && readOptions .isExecutorCredentialRefresh ()) {
7883 if (LanceRuntime .useNamespaceOnWorkers (inputPartition .getNamespaceImpl ())) {
7984 readOptions .setNamespace (
@@ -97,31 +102,34 @@ public static LanceFragmentScanner create(int fragmentId, LanceInputPartition in
97102 fragmentId , readOptions .getDatasetUri (), readOptions .getVersion ()));
98103 }
99104 ScanOptions .Builder scanOptions = new ScanOptions .Builder ();
105+
106+ // Detect blob columns in the schema
107+ Set <String > blobColumnNames = getBlobColumnNames (inputPartition .getSchema ());
108+ boolean hasBlobColumns = !blobColumnNames .isEmpty ();
109+
100110 List <String > projectedColumns = getColumnNames (inputPartition .getSchema ());
101111 if (projectedColumns .isEmpty () && inputPartition .getSchema ().isEmpty ()) {
102- // Lance requires at least one projected column. Use _rowid as a lightweight
103- // sentinel so the scanner still returns the correct row count (e.g. SELECT 1).
104112 scanOptions .withRowId (true );
105113 }
106114 if (hasField (inputPartition .getSchema (), LanceConstant .ROW_ID )) {
107115 scanOptions .withRowId (true );
108116 }
109- if (hasField (inputPartition .getSchema (), LanceConstant .ROW_ADDRESS )) {
117+
118+ // Request _rowaddr when blob columns are present so we can build blob references.
119+ boolean userRequestedRowAddr =
120+ hasField (inputPartition .getSchema (), LanceConstant .ROW_ADDRESS );
121+ boolean withRowAddrForBlobs = hasBlobColumns && !userRequestedRowAddr ;
122+ if (hasBlobColumns || userRequestedRowAddr ) {
110123 scanOptions .withRowAddress (true );
111124 }
125+
112126 scanOptions .columns (projectedColumns );
113127 if (inputPartition .getWhereCondition ().isPresent ()) {
114128 scanOptions .filter (inputPartition .getWhereCondition ().get ());
115129 }
116130 scanOptions .batchSize (readOptions .getBatchSize ());
117131 if (readOptions .getNearest () != null ) {
118132 scanOptions .nearest (readOptions .getNearest ());
119- // We strictly set `prefilter = true` here to ensure query correctness.
120- // This is necessary due to the combination of two factors:
121- // 1. Spark currently performs the vector search by individually scanning each fragment.
122- // 2. Lance mandates that `prefilter` must be enabled for fragmented vector queries.
123- // If Spark's execution model or Lance's search functionality changes in the future,
124- // we need to revisit this.
125133 scanOptions .prefilter (true );
126134 }
127135 if (inputPartition .getLimit ().isPresent ()) {
@@ -145,7 +153,9 @@ public static LanceFragmentScanner create(int fragmentId, LanceInputPartition in
145153 withFragmentId ,
146154 inputPartition ,
147155 dsOpenTimeNs ,
148- scanCreateTimeNs );
156+ scanCreateTimeNs ,
157+ withRowAddrForBlobs ,
158+ blobColumnNames );
149159 } catch (Throwable throwable ) {
150160 if (lanceScanner != null ) {
151161 try {
@@ -211,8 +221,8 @@ public int fragmentId() {
211221 return fragmentId ;
212222 }
213223
214- public boolean withFragemtId () {
215- return withFragemtId ;
224+ public boolean withFragmentId () {
225+ return withFragmentId ;
216226 }
217227
218228 public LanceInputPartition getInputPartition () {
@@ -227,19 +237,37 @@ public long getScannerCreateTimeNs() {
227237 return scannerCreateTimeNs ;
228238 }
229239
230- /**
231- * Builds the projection column list for the scanner. Row ID and row address are requested through
232- * explicit scan flags so Lance computes them from the active fragment metadata instead of reading
233- * them as regular columns.
234- */
240+ /** Whether the scanner implicitly requested _rowaddr for blob reference support. */
241+ public boolean isWithRowAddrForBlobs () {
242+ return withRowAddrForBlobs ;
243+ }
244+
245+ /** Returns the blob column names in the projected schema. */
246+ public Set <String > getBlobColumnNames () {
247+ return blobColumnNames ;
248+ }
249+
250+ /** Returns the dataset URI for blob references. */
251+ public String getDatasetUri () {
252+ return inputPartition .getReadOptions ().getDatasetUri ();
253+ }
254+
255+ private static Set <String > getBlobColumnNames (StructType schema ) {
256+ Set <String > blobColumns = new HashSet <>();
257+ for (StructField field : schema .fields ()) {
258+ if (BlobUtils .isBlobSparkField (field )) {
259+ blobColumns .add (field .name ());
260+ }
261+ }
262+ return blobColumns ;
263+ }
264+
235265 private static List <String > getColumnNames (StructType schema ) {
236- // Collect all field names in the schema for quick lookup
237266 java .util .Set <String > schemaFields = new java .util .HashSet <>();
238267 for (StructField field : schema .fields ()) {
239268 schemaFields .add (field .name ());
240269 }
241270
242- // Regular data columns (exclude all special/metadata columns)
243271 List <String > columns =
244272 Arrays .stream (schema .fields ())
245273 .map (StructField ::name )
0 commit comments