malinjawi commented on code in PR #13554:
URL: https://github.com/apache/gluten/pull/13554#discussion_r4492342707


##########
backends-velox/src-lance/main/scala/org/apache/gluten/execution/LanceScanTransformer.scala:
##########
@@ -105,14 +110,74 @@ object LanceScanTransformer {
    * rejects both at runtime (checkNoPushedAggregation / 
checkNativeSchemaMatchesPartition), but by
    * then the node is already offloaded to Velox, so the throw fails the query 
instead of falling
    * back. Detecting them here keeps such scans on vanilla Spark (e.g. the 
fast COUNT(*) metadata
-   * reader) rather than offloading a scan that can only crash.
+   * reader) rather than offloading a scan that can only crash. The same holds 
for the columns
+   * checked by [[hasExportableColumns]].
    */
   def isExportable(batchScan: BatchScanExec): Boolean =
     batchScan.inputPartitions.forall {
       case p: LanceInputPartition =>
         !p.getPushedAggregation.isPresent && p.getReadOptions.getFullTextQuery 
== null
       case _ => false
+    } && hasExportableColumns(batchScan.scan.readSchema())
+
+  /**
+   * Whether the native fragment scan returns the columns of `schema` as 
declared, each as an Arrow
+   * vector that Spark's [[ArrowColumnVector]], which each imported vector is 
wrapped in, reads as
+   * the declared type.
+   *
+   * [[LanceArrowStreamScanner.export]] streams the native scan as is, and 
only if its schema equals
+   * the one lance-spark's Spark-to-Arrow mapping ([[LanceArrowUtils]]) 
derives from the declared
+   * schema. The native scan differs for a scan that reads no columns, where 
it returns `_rowid`,
+   * and for the columns in [[needsLanceReader]]. Every other column arrives 
with its mapped type,
+   * with vectors rewritten as Lists ([[FixedSizeLists]]), and 
ArrowColumnVector has no accessor for
+   * some of these types, e.g. FixedSizeBinary, Date(MILLISECOND) and the 
elements of a Float16
+   * vector. All of these fail at execution, after the scan was offloaded. A 
column's type is
+   * checked by wrapping an empty vector of its field as the scan emits it, 
which resolves the type
+   * as the imported batches will.
+   */
+  private def hasExportableColumns(schema: StructType): Boolean =
+    schema.nonEmpty && !schema.fields.exists(needsLanceReader) &&
+      // The mapping throws for types it rejects (e.g. Float16 without Arrow's 
Float2Vector), and
+      // the export runs the same mapping.
+      Try(LanceArrowUtils.toArrowSchema(schema, "UTC", true)).toOption.exists {
+        arrowSchema =>
+          schema.fields.zip(arrowSchema.getFields.asScala).forall {
+            case (field, arrowField) =>
+              isWrappable(FixedSizeLists.toListField(arrowField), 
field.dataType)
+          }
+      }
+
+  /**
+   * Whether only lance-spark's own reader returns `field` as declared, by 
fixing up the native scan
+   * after import. The native scan returns blob columns as blob descriptions 
alongside `_rowaddr`,
+   * and JSON columns as JSONB bytes. Lance's metadata columns, and the blob 
position and size
+   * columns, are left out of the native scan's projection: lance-spark 
requests, computes or
+   * reorders them itself.
+   */
+  private def needsLanceReader(field: StructField): Boolean =

Review Comment:
   Good catch, thanks! I missed that lance-spark keeps those markers on nested 
fields too. Fixed in c32f49a41: the check now looks inside structs, lists and 
maps, and falls back if it finds a blob or JSON field anywhere. I added tests 
for JSON in a struct, in a list of structs and as a map value, and for a blob 
inside a struct.
   



##########
backends-velox/src-lance/main/scala/org/apache/gluten/execution/LanceScanTransformer.scala:
##########
@@ -105,14 +110,74 @@ object LanceScanTransformer {
    * rejects both at runtime (checkNoPushedAggregation / 
checkNativeSchemaMatchesPartition), but by
    * then the node is already offloaded to Velox, so the throw fails the query 
instead of falling
    * back. Detecting them here keeps such scans on vanilla Spark (e.g. the 
fast COUNT(*) metadata
-   * reader) rather than offloading a scan that can only crash.
+   * reader) rather than offloading a scan that can only crash. The same holds 
for the columns
+   * checked by [[hasExportableColumns]].
    */
   def isExportable(batchScan: BatchScanExec): Boolean =
     batchScan.inputPartitions.forall {
       case p: LanceInputPartition =>
         !p.getPushedAggregation.isPresent && p.getReadOptions.getFullTextQuery 
== null
       case _ => false
+    } && hasExportableColumns(batchScan.scan.readSchema())
+
+  /**
+   * Whether the native fragment scan returns the columns of `schema` as 
declared, each as an Arrow
+   * vector that Spark's [[ArrowColumnVector]], which each imported vector is 
wrapped in, reads as
+   * the declared type.
+   *
+   * [[LanceArrowStreamScanner.export]] streams the native scan as is, and 
only if its schema equals
+   * the one lance-spark's Spark-to-Arrow mapping ([[LanceArrowUtils]]) 
derives from the declared
+   * schema. The native scan differs for a scan that reads no columns, where 
it returns `_rowid`,
+   * and for the columns in [[needsLanceReader]]. Every other column arrives 
with its mapped type,
+   * with vectors rewritten as Lists ([[FixedSizeLists]]), and 
ArrowColumnVector has no accessor for
+   * some of these types, e.g. FixedSizeBinary, Date(MILLISECOND) and the 
elements of a Float16
+   * vector. All of these fail at execution, after the scan was offloaded. A 
column's type is
+   * checked by wrapping an empty vector of its field as the scan emits it, 
which resolves the type
+   * as the imported batches will.
+   */
+  private def hasExportableColumns(schema: StructType): Boolean =
+    schema.nonEmpty && !schema.fields.exists(needsLanceReader) &&
+      // The mapping throws for types it rejects (e.g. Float16 without Arrow's 
Float2Vector), and
+      // the export runs the same mapping.
+      Try(LanceArrowUtils.toArrowSchema(schema, "UTC", true)).toOption.exists {
+        arrowSchema =>
+          schema.fields.zip(arrowSchema.getFields.asScala).forall {
+            case (field, arrowField) =>
+              isWrappable(FixedSizeLists.toListField(arrowField), 
field.dataType)

Review Comment:
   Makes sense, done in c32f49a41. If a field has `arrow.fixed-size-list.size` 
but doesn't map back to a `FixedSizeList`, the scan now falls back, at any 
depth. I tested an int8 vector and a vector of vectors, written with lance-core 
since lance-spark can't write them. And as you said, they'll start offloading 
again on their own once lance-spark maps them as `FixedSizeList`.
   



##########
docs/backends/velox/get-started/VeloxLance.md:
##########
@@ -82,3 +86,6 @@ engages automatically whenever lance-spark is present on the 
classpath.
   (in the Lance reader) before the Arrow stream is exported, not inside Velox.
 - Rare schemas that require JVM post-processing (for example `_rowid` / 
`_rowaddr` / blob columns)
   are not offloaded.
+- With lance-spark 0.8.0, a scan over an unsigned integer, `LargeList`, time, 
or non-microsecond

Review Comment:
   Good one, added it to the list. Thanks!
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to