HyukjinKwon commented on code in PR #59313:
URL: https://github.com/apache/spark/pull/59313#discussion_r4237217949


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceRegistry.scala:
##########
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.execution.datasources.v2.ffi
+
+import java.io.File
+import java.nio.file.Paths
+import java.util.Locale
+
+import org.apache.spark.{SparkEnv, SparkFiles}
+import org.apache.spark.internal.Logging
+import org.apache.spark.internal.LogKeys.PATH
+import org.apache.spark.sql.Artifact
+import org.apache.spark.sql.classic.SparkSession
+import org.apache.spark.sql.datasource.NativeBridge
+import org.apache.spark.sql.errors.{QueryCompilationErrors, 
QueryExecutionErrors}
+import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
+import org.apache.spark.util.Utils
+
+/**
+ * Finds the native data sources on the driver. Like the Python data sources 
installed in the
+ * Python path, the native data source libraries installed in the native 
library path are found
+ * automatically, by their file name: the library `spark_datasource_<name>`, 
such as
+ * `libspark_datasource_my_source.so` on Linux, implements the data source 
`<name>`. The active
+ * session can also add packages with `spark.addArtifact`, or with
+ * `spark.sql.dataSource.native.paths`, which take precedence over the 
installed libraries.
+ */
+object NativeDataSourceRegistry extends Logging {
+
+  /** The prefix of the names of the installed native data source libraries. */
+  val LIBRARY_NAME_PREFIX = "spark_datasource_"
+
+  // The result of the last lookup by `DataSource.lookupDataSource` on this 
thread that found a
+  // native data source, by the name it looked up. The provider that the 
caller creates next for
+  // that name takes it, so that it does not look the name up again: see 
[[takeLookup]].
+  private val lastLookup = new ThreadLocal[(String, (String, 
NativeLibraryLocation))]
+
+  /**
+   * Finds the package or the installed library that provides the native data 
source with the
+   * given name, which is case-insensitive. Returns the name of the data 
source, as listed in the
+   * manifest of the package or in lower case for an installed library, and 
where its library is.
+   */
+  def lookup(name: String, conf: SQLConf): Option[(String, 
NativeLibraryLocation)] = {
+    if (!conf.getConf(StaticSQLConf.NATIVE_DATA_SOURCE_ENABLED)) {
+      return None
+    }
+    lookupPackage(name, configuredPackageFiles(conf) ++ artifactPackageFiles())
+      .orElse(lookupInstalledLibrary(name))
+  }
+
+  /**
+   * Like [[lookup]], and keeps the result for the provider that the caller 
creates next for the
+   * given name, which takes it with [[takeLookup]].
+   */
+  def lookupForProvider(name: String, conf: SQLConf): Option[(String, 
NativeLibraryLocation)] = {
+    val result = lookup(name, conf)
+    result.foreach(found => lastLookup.set(name -> found))
+    result
+  }
+
+  /** Takes the result of [[lookupForProvider]] for the given name on this 
thread, if any. */
+  def takeLookup(name: String): Option[(String, NativeLibraryLocation)] = {
+    Option(lastLookup.get()).filter(_._1.equalsIgnoreCase(name)).map { case 
(_, found) =>
+      lastLookup.remove()
+      found
+    }
+  }
+
+  private def lookupPackage(
+      name: String,
+      files: Seq[File]): Option[(String, NativeDataSourcePackage)] = {
+    val (invalid, valid) = 
files.map(NativeDataSourcePackage.tryRead).partitionMap(identity)
+    // An invalid package does not prevent finding the data sources of the 
other packages, unless
+    // its manifest lists the data source. An invalid package is logged when 
it is read.
+    invalid.find(_.dataSources.exists(_.equalsIgnoreCase(name))).foreach(p => 
throw p.error)
+    val found = valid
+      .flatMap(pkg => 
pkg.manifest.dataSources.find(_.equalsIgnoreCase(name)).map(_ -> pkg))
+      // The same package can be found more than once, for example when it is 
in a configured
+      // directory and was also added as an artifact.
+      .groupBy(_._2.sha256).values.map(_.head).toSeq
+    found match {
+      case Seq() => None
+      case Seq(result @ (_, pkg)) =>
+        if (pkg.manifest.abiVersion != NativeBridge.ABI_VERSION) {
+          throw QueryExecutionErrors.invalidNativeDataSourcePackageError(
+            pkg.path,
+            "UNSUPPORTED_ABI_VERSION",
+            Map(
+              "version" -> pkg.manifest.abiVersion.toString,
+              "supported" -> NativeBridge.ABI_VERSION.toString))
+        }
+        Some(result)
+      case _ =>
+        throw QueryCompilationErrors.nativeDataSourcePackageConflictError(
+          name, found.map(_._2.path).sorted)
+    }
+  }
+
+  private def lookupInstalledLibrary(name: String): Option[(String, 
InstalledNativeLibrary)] = {
+    // Other names, such as class names, are not the names of libraries.
+    if (!name.matches("[A-Za-z0-9_]+")) {
+      return None
+    }
+    val dataSource = name.toLowerCase(Locale.ROOT)
+    findInstalledLibrary(libraryFileName(dataSource))
+      .map(file => dataSource -> InstalledNativeLibrary(file.getPath))
+  }
+
+  /** Finds the library with the given file name in the native library path of 
this node. */
+  private[ffi] def findInstalledLibrary(fileName: String): Option[File] = {
+    librarySearchPath(System.getProperty("java.library.path"), 
System.getenv("PATH"))
+      .map(new File(_, fileName))
+      .find(_.isFile)
+  }
+
+  /**
+   * The file name of the installed library of the native data source with the 
given name, such
+   * as `libspark_datasource_my_source.so` on Linux, 
`libspark_datasource_my_source.dylib` on
+   * macOS and `spark_datasource_my_source.dll` on Windows.
+   */
+  def libraryFileName(name: String): String = {
+    System.mapLibraryName(LIBRARY_NAME_PREFIX + name.toLowerCase(Locale.ROOT))
+  }
+
+  /**
+   * The native library path: the directories where native data source 
libraries are installed,
+   * in the order they are searched. They are the directories of 
`java.library.path`, where the
+   * JVM finds native libraries, which include `LD_LIBRARY_PATH` on Linux, 
`DYLD_LIBRARY_PATH` on
+   * macOS and `PATH` on Windows, and then the `lib` directory of each 
installation prefix in
+   * `PATH`, such as `/usr/local/lib` for `/usr/local/bin`. Relative 
directories are ignored.
+   */
+  private[ffi] def librarySearchPath(javaLibraryPath: String, path: String): 
Seq[File] = {
+    def directories(value: String): Seq[File] = {
+      Option(value).toSeq.flatMap(_.split(File.pathSeparator)).map(new 
File(_)).filter(_.isAbsolute)
+    }
+    val prefixLibraries = directories(path)
+      .filter(_.getName == "bin")
+      .flatMap(bin => Option(bin.getParentFile))
+      .map(new File(_, "lib"))
+    (directories(javaLibraryPath) ++ prefixLibraries).distinct
+  }
+
+  /**
+   * Makes the library available to the executors of the active session, which 
runs the jobs of
+   * the scan or the write: for a streaming query, it is the session of the 
query, a clone of the
+   * session that started it. Returns where the executors find it. An 
installed library is already
+   * installed on every node.
+   */
+  def distribute(location: NativeLibraryLocation): NativeLibraryLocation = 
location match {
+    case pkg: NativeDataSourcePackage =>
+      SparkSession.getActiveSession match {
+        // In local mode, the executors run in the driver and read the package 
where it is.
+        case Some(session) if !session.sparkContext.isLocal => 
addToArtifacts(session, pkg)
+        case _ => pkg

Review Comment:
   Done in 9ac5a49. `distribute` now has separate cases: local mode returns the 
package, a non-local session copies it to that session's artifacts, and no 
active session logs a warning naming the package. The warning says the 
executors will only find the package if it's at the same path on every node 
(test: `distribute a package without an active session`). On the lookup side, 
@LuciferYang found a real path where `artifactPackageFiles()` reads the wrong 
session: classic `save()`/`start()` resolve the provider outside `withActive`. 
That's fixed in the same commit, see the reply on that thread.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSource.scala:
##########
@@ -0,0 +1,655 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.execution.datasources.v2.ffi
+
+import java.lang.ref.{Cleaner, WeakReference}
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicLong
+
+import scala.jdk.CollectionConverters._
+import scala.util.control.NonFatal
+
+import org.apache.arrow.c.{ArrowArray, ArrowArrayStream, ArrowSchema, Data}
+import org.apache.arrow.vector.{FieldVector, VectorSchemaRoot}
+import org.apache.arrow.vector.ipc.ArrowReader
+import org.apache.arrow.vector.types.pojo.Field
+
+import org.apache.spark.{SparkException, SparkThrowable, 
SparkUnsupportedOperationException}
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.connector.expressions.filter.Predicate
+import org.apache.spark.sql.connector.read.{InputPartition, PartitionReader}
+import org.apache.spark.sql.connector.write.{DataWriter, WriterCommitMessage}
+import org.apache.spark.sql.errors.{QueryCompilationErrors, 
QueryExecutionErrors}
+import 
org.apache.spark.sql.execution.datasources.v2.columnar.{ColumnarDataSource, 
ColumnarReader, ColumnarStreamReader, ColumnarStreamWriter, ColumnarWriter}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.{DataType, StructType}
+import org.apache.spark.sql.util.{ArrowUtils, CaseInsensitiveStringMap}
+import org.apache.spark.sql.vectorized.{ArrowColumnVector, ColumnarBatch, 
ColumnVector}
+import org.apache.spark.util.Utils
+
+/**
+ * A [[ColumnarDataSource]] implemented by a native library, through
+ * `org.apache.spark.sql.datasource.NativeBridge`.
+ *
+ * @param location where the library is on the driver. The readers and the 
writers distribute it
+ *                 to the executors of the active session when they are 
created.
+ * @param name the name of the data source, as listed in the manifest of its 
package or in lower
+ *             case for an installed library
+ */
+class NativeDataSource(
+    location: NativeLibraryLocation,
+    name: String,
+    options: CaseInsensitiveStringMap)
+  extends ColumnarDataSource {
+  import NativeCalls._
+
+  override def schema(): StructType = {
+    val library = NativeLibraries.get(location)
+    withDataSource(library, "infer the schema of") { dataSource =>
+      call(name, "infer the schema of") {
+        optional(NativeArrow.importSchema(address => 
library.schema(dataSource, address)))
+      }.getOrElse(throw 
QueryCompilationErrors.dataSchemaNotSpecifiedError(name))
+    }
+  }
+
+  override def reader(schema: StructType): ColumnarReader = {
+    val executorLocation = NativeDataSourceRegistry.distribute(location)
+    val library = NativeLibraries.get(location)
+    val handle = create(library, "plan a scan of", 
"DATA_SOURCE_BATCH_SCAN_NOT_SUPPORTED") {
+      dataSource =>
+        NativeArrow.exportSchema(schema)(address => 
library.createReader(dataSource, address))
+    }
+    new NativeDataSourceReader(
+      executorLocation, name, schema, new NativeHandle(handle, 
library.closeReader))
+  }
+
+  override def streamReader(schema: StructType): ColumnarStreamReader = {
+    val executorLocation = NativeDataSourceRegistry.distribute(location)
+    val library = NativeLibraries.get(location)
+    val handle = create(library, "plan a scan of", 
"DATA_SOURCE_MICRO_BATCH_SCAN_NOT_SUPPORTED") {
+      dataSource =>
+        NativeArrow.exportSchema(schema)(address => 
library.createStreamReader(dataSource, address))
+    }
+    new NativeDataSourceStreamReader(
+      executorLocation, name, schema, new NativeHandle(handle, 
library.closeStreamReader))
+  }
+
+  override def writer(schema: StructType, overwrite: Boolean): ColumnarWriter 
= {
+    val executorLocation = NativeDataSourceRegistry.distribute(location)
+    val library = NativeLibraries.get(location)
+    val handle = new NativeHandle(
+      create(library, "plan a write to", 
"DATA_SOURCE_BATCH_WRITE_NOT_SUPPORTED") { dataSource =>
+        NativeArrow.exportSchema(schema) { address =>
+          library.createWriter(dataSource, address, overwrite)
+        }
+      },
+      library.closeWriter)
+    new NativeDataSourceWriter(executorLocation, name, handle, 
serializeWriter(library, handle))
+  }
+
+  override def streamWriter(
+      schema: StructType,
+      overwrite: Boolean,
+      queryId: String): ColumnarStreamWriter = {
+    val executorLocation = NativeDataSourceRegistry.distribute(location)
+    val library = NativeLibraries.get(location)
+    val handle = new NativeHandle(
+      create(library, "plan a write to", 
"DATA_SOURCE_STREAMING_WRITE_NOT_SUPPORTED") {
+        dataSource =>
+          NativeArrow.exportSchema(schema) { address =>
+            library.createStreamWriter(dataSource, address, overwrite)
+          }
+      },
+      library.closeWriter)
+    val writer = new NativeDataSourceStreamWriter(
+      executorLocation, name, handle, serializeWriter(library, handle))
+    NativeStreamWriters.register(queryId, handle)
+    writer
+  }
+
+  /** Runs `f` with a new native data source, which is closed afterwards. */
+  private def withDataSource[T](library: NativeLibrary, action: String)(f: 
Long => T): T = {
+    // The keys of a case-insensitive map are in lower case.
+    val entries = options.entrySet().asScala.toArray
+    val dataSource = call(name, "create") {
+      library.createDataSource(name, entries.map(_.getKey), 
entries.map(_.getValue))
+    }
+    Utils.tryWithSafeFinally(f(dataSource)) {
+      call(name, action)(library.closeDataSource(dataSource))
+    }
+  }
+
+  /** Creates a reader or a writer with `f`, which fails if the library does 
not implement it. */
+  private def create(library: NativeLibrary, action: String, unsupportedError: 
String)(
+      f: Long => Long): Long = {
+    withDataSource(library, action) { dataSource =>
+      call(name, action)(optional(f(dataSource))).getOrElse {
+        throw new SparkUnsupportedOperationException(unsupportedError, 
Map("description" -> name))
+      }
+    }
+  }
+
+  private def serializeWriter(library: NativeLibrary, handle: NativeHandle): 
Array[Byte] = {
+    try {
+      bytesOrEmpty(call(name, "plan a write 
to")(optional(library.serializeWriter(handle.get))))
+    } catch {
+      case e: Throwable =>
+        handle.close()
+        throw e
+    }
+  }
+}
+
+/** Reads a native data source for a batch scan. */
+class NativeDataSourceReader(
+    location: NativeLibraryLocation,
+    name: String,
+    schema: StructType,
+    @transient private val handle: NativeHandle)
+  extends ColumnarReader {
+  import NativeCalls._
+
+  // Set on the driver when the partitions are planned, and serialized to the 
executors.
+  private var readerState: Array[Byte] = Array.emptyByteArray
+  private var readSchema: StructType = schema
+  @transient private var plannedPartitions: Array[InputPartition] = _
+
+  @transient private lazy val library = NativeLibraries.get(location)
+
+  override def pushPredicates(predicates: Array[Predicate]): Array[Predicate] 
= {
+    // Only the predicates that can be expressed in JSON are passed to the 
library.
+    val encoded = predicates.zipWithIndex.flatMap { case (predicate, i) =>
+      NativePredicates.toJson(predicate, schema).map(_ -> i)
+    }
+    val pushed = if (encoded.isEmpty) {
+      Set.empty[Int]
+    } else {
+      call(name, "plan a scan of") {
+        optional(library.pushPredicates(handle.get, encoded.map(_._1)))
+      } match {
+        case Some(flags) if flags != null && flags.length == encoded.length =>
+          encoded.map(_._2).zip(flags).collect { case (i, true) => i }.toSet
+        case Some(flags) =>
+          throw error(name, "plan a scan of", s"pushPredicates returned " +
+            s"${Option(flags).map(_.length).getOrElse(0)} values for 
${encoded.length} predicates.")
+        case None => Set.empty[Int]
+      }
+    }
+    predicates.zipWithIndex.collect { case (predicate, i) if 
!pushed.contains(i) => predicate }
+  }
+
+  override def pushLimit(limit: Int): Boolean = {
+    call(name, "plan a scan of")(optional(library.pushLimit(handle.get, 
limit))).getOrElse(false)
+  }
+
+  override def pruneColumns(requiredSchema: StructType): Boolean = {
+    val pruned = call(name, "plan a scan of") {
+      optional(library.pruneColumns(handle.get, requiredSchema.fieldNames))
+    }.getOrElse(false)
+    if (pruned) {
+      readSchema = requiredSchema
+    }
+    pruned
+  }
+
+  override def partitions(): Array[InputPartition] = synchronized {
+    if (plannedPartitions == null) {
+      try {
+        val partitions = call(name, "plan a scan 
of")(library.partitions(handle.get))
+        readerState = bytesOrEmpty(
+          call(name, "plan a scan 
of")(optional(library.serializeReader(handle.get))))
+        plannedPartitions = NativeInputPartition.from(partitions)
+      } finally {
+        // The executors only need the serialized state.
+        handle.close()
+      }
+    }
+    plannedPartitions
+  }
+
+  override def read(partition: InputPartition): PartitionReader[ColumnarBatch] 
= {
+    new NativePartitionReader(
+      location, name, readerState, 
partition.asInstanceOf[NativeInputPartition].bytes, readSchema)
+  }
+}
+
+/** Reads a native data source for a micro-batch streaming scan. */
+class NativeDataSourceStreamReader(
+    location: NativeLibraryLocation,
+    name: String,
+    schema: StructType,
+    @transient private val handle: NativeHandle)
+  extends ColumnarStreamReader {
+  import NativeCalls._
+
+  // Set on the driver whenever the partitions of a micro-batch are planned, 
and serialized to
+  // the executors with the partitions of that micro-batch.
+  private var readerState: Array[Byte] = Array.emptyByteArray
+
+  @transient private lazy val library = NativeLibraries.get(location)
+
+  override def initialOffset(): String = {
+    call(name, "plan a scan of")(library.initialOffset(handle.get))
+  }
+
+  override def latestOffset(): String = {
+    call(name, "plan a scan of")(library.latestOffset(handle.get))
+  }
+
+  override def partitions(start: String, end: String): Array[InputPartition] = 
{
+    val partitions = call(name, "plan a scan of") {
+      library.streamPartitions(handle.get, start, end)
+    }
+    readerState = bytesOrEmpty(
+      call(name, "plan a scan 
of")(optional(library.serializeStreamReader(handle.get))))
+    NativeInputPartition.from(partitions)
+  }
+
+  override def read(partition: InputPartition): PartitionReader[ColumnarBatch] 
= {
+    new NativePartitionReader(
+      location, name, readerState, 
partition.asInstanceOf[NativeInputPartition].bytes, schema)
+  }
+
+  override def commit(end: String): Unit = {
+    call(name, "commit an offset 
of")(optional(library.commitOffset(handle.get, end)))
+  }
+
+  override def stop(): Unit = handle.close()
+}
+
+/** Writes to a native data source for a batch write. */
+class NativeDataSourceWriter(
+    location: NativeLibraryLocation,
+    name: String,
+    @transient private val handle: NativeHandle,
+    writerState: Array[Byte])
+  extends ColumnarWriter {
+  import NativeCalls._
+
+  @transient private lazy val library = NativeLibraries.get(location)
+
+  override def createWriter(partitionId: Int, taskId: Long): 
DataWriter[ColumnarBatch] = {
+    new NativeDataWriter(location, name, writerState, partitionId, taskId, 
epochId = -1L)
+  }
+
+  override def commit(messages: Array[WriterCommitMessage]): Unit = {
+    call(name, "commit a write to") {
+      library.commit(handle.get, -1L, 
NativeWriterCommitMessage.toBytes(messages))
+    }
+    handle.close()

Review Comment:
   Thanks for checking. Here the asymmetry is deliberate: 
`V2TableWriteExec.writeWithV2` calls `batchWrite.commit` inside the `try`, and 
its `catch` calls `batchWrite.abort(messages)`. So when `library.commit` 
throws, Spark calls `abort`, which uses the handle and then closes it in its 
`tryWithSafeFinally`. If `commit` closed the handle in a `finally`, that 
`abort` would fail with `The native handle was already released` (added as a 
suppressed error), and the library's `abort` would never run. In 9ac5a49 I 
added a comment on that line and a sentence to the `NativeBridge` lifecycle 
Javadoc ("...or abort otherwise, including when commit fails, and closes the 
writer"). The new test `abort a write whose commit failed` covers it: 
`fail=commit` produces the commit error with no suppressed error, and the test 
library recorded both `_ABORTED` and `_CLOSED`.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/StaticSQLConf.scala:
##########
@@ -464,6 +464,22 @@ object StaticSQLConf {
       .checkValue(_ >= 0, "The maximum total size must not be negative.")
       .createWithDefault(128 * 1024) // 128 KiB
 
+  val NATIVE_DATA_SOURCE_ENABLED =
+    buildStaticConf("spark.sql.dataSource.native.enabled")
+      .doc("When true, Spark finds and loads native data sources: data sources 
implemented in " +
+        "native code such as Rust or C++. The library of a data source, such 
as my_source, is " +
+        "found when it is installed in the native library path as 
spark_datasource_my_source, " +
+        "for example libspark_datasource_my_source.so in /usr/local/lib on 
Linux. The native " +
+        "library path consists of the directories of java.library.path, and 
the lib directory " +
+        "of each installation prefix in PATH. Libraries can also be 
distributed in native data " +
+        "source packages (.sparkpkg files), added with spark.addArtifact or 
under " +
+        "spark.sql.dataSource.native.paths. Loading a native data source runs 
its native code " +
+        "in the driver and executor processes.")
+      .version("4.4.0")
+      .withBindingPolicy(ConfigBindingPolicy.NOT_APPLICABLE)
+      .booleanConf
+      .createWithDefault(true)

Review Comment:
   Thanks, this is worth spelling out. I kept `true` on purpose. It matches the 
Python data sources: Spark already imports the `pyspark_*` packages installed 
in the Python path automatically, with no switch, whenever Python is available, 
which is more eager than this. A native library is loaded only when a query 
names it and no Java or Python data source has that name, and only from 
directories that should already be trusted, since the JVM itself loads 
libraries from `java.library.path`. In 9ac5a49 I expanded the existing Security 
section of `docs/sql-native-data-sources.md`. It now says that libraries are 
found by file name on the native library path and loaded when a query uses the 
name, the same way Python `pyspark_*` packages are picked up. It says not to 
let untrusted users write to those directories or to 
`spark.sql.dataSource.native.paths`. It notes that anyone who can add artifacts 
(e.g. a Spark Connect client) can add a package, just as they can add JARs. And 
it recommends `
 spark.sql.dataSource.native.enabled=false` for shared servers.



##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/v2/ffi/NativeLibrary.java:
##########
@@ -0,0 +1,292 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.execution.datasources.v2.ffi;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.apache.spark.sql.datasource.NativeBridge;
+
+/**
+ * A native data source library, loaded into its own copy of {@link 
NativeBridge}.
+ * <p>
+ * JNI links the native methods of a class to the libraries loaded by the 
class loader of that
+ * class, and a library belongs to the class loader of the class that loads 
it. Defining a copy of
+ * {@link NativeBridge} in a separate class loader for each library therefore 
lets all the
+ * libraries export the same JNI functions. Spark calls the methods of a copy 
through method
+ * handles, because the copy is a different class than the {@link 
NativeBridge} it is compiled
+ * against. Calling a method that the library does not export throws
+ * {@link UnsatisfiedLinkError}.
+ */
+final class NativeLibrary {
+
+  private static final String BRIDGE_CLASS = NativeBridge.class.getName();
+
+  private final MethodHandle abiVersion;
+  private final MethodHandle createDataSource;
+  private final MethodHandle schema;
+  private final MethodHandle createReader;
+  private final MethodHandle createStreamReader;
+  private final MethodHandle createWriter;
+  private final MethodHandle createStreamWriter;
+  private final MethodHandle closeDataSource;
+  private final MethodHandle pushPredicates;
+  private final MethodHandle pushLimit;
+  private final MethodHandle pruneColumns;
+  private final MethodHandle partitions;
+  private final MethodHandle serializeReader;
+  private final MethodHandle closeReader;
+  private final MethodHandle initialOffset;
+  private final MethodHandle latestOffset;
+  private final MethodHandle streamPartitions;
+  private final MethodHandle serializeStreamReader;
+  private final MethodHandle commitOffset;
+  private final MethodHandle closeStreamReader;
+  private final MethodHandle read;
+  private final MethodHandle serializeWriter;
+  private final MethodHandle commit;
+  private final MethodHandle abort;
+  private final MethodHandle closeWriter;
+  private final MethodHandle createDataWriter;
+  private final MethodHandle write;
+  private final MethodHandle commitDataWriter;
+  private final MethodHandle abortDataWriter;
+
+  private NativeLibrary(Class<?> bridge) throws ReflectiveOperationException {
+    Class<?> jlong = long.class;
+    Class<?> bytes = byte[].class;
+    Class<?> strings = String[].class;
+    abiVersion = find(bridge, "abiVersion", int.class);
+    createDataSource = find(bridge, "createDataSource", jlong, String.class, 
strings, strings);
+    schema = find(bridge, "schema", void.class, jlong, jlong);
+    createReader = find(bridge, "createReader", jlong, jlong, jlong);
+    createStreamReader = find(bridge, "createStreamReader", jlong, jlong, 
jlong);
+    createWriter = find(bridge, "createWriter", jlong, jlong, jlong, 
boolean.class);
+    createStreamWriter = find(bridge, "createStreamWriter", jlong, jlong, 
jlong, boolean.class);
+    closeDataSource = find(bridge, "closeDataSource", void.class, jlong);
+    pushPredicates = find(bridge, "pushPredicates", boolean[].class, jlong, 
strings);
+    pushLimit = find(bridge, "pushLimit", boolean.class, jlong, int.class);
+    pruneColumns = find(bridge, "pruneColumns", boolean.class, jlong, strings);
+    partitions = find(bridge, "partitions", byte[][].class, jlong);
+    serializeReader = find(bridge, "serializeReader", bytes, jlong);
+    closeReader = find(bridge, "closeReader", void.class, jlong);
+    initialOffset = find(bridge, "initialOffset", String.class, jlong);
+    latestOffset = find(bridge, "latestOffset", String.class, jlong);
+    streamPartitions =
+      find(bridge, "streamPartitions", byte[][].class, jlong, String.class, 
String.class);
+    serializeStreamReader = find(bridge, "serializeStreamReader", bytes, 
jlong);
+    commitOffset = find(bridge, "commitOffset", void.class, jlong, 
String.class);
+    closeStreamReader = find(bridge, "closeStreamReader", void.class, jlong);
+    read = find(bridge, "read", void.class, bytes, bytes, jlong);
+    serializeWriter = find(bridge, "serializeWriter", bytes, jlong);
+    commit = find(bridge, "commit", void.class, jlong, jlong, byte[][].class);
+    abort = find(bridge, "abort", void.class, jlong, jlong, byte[][].class);
+    closeWriter = find(bridge, "closeWriter", void.class, jlong);
+    createDataWriter = find(bridge, "createDataWriter", jlong, bytes, 
int.class, jlong, jlong);
+    write = find(bridge, "write", void.class, jlong, jlong, jlong);
+    commitDataWriter = find(bridge, "commitDataWriter", bytes, jlong);
+    abortDataWriter = find(bridge, "abortDataWriter", void.class, jlong);
+  }
+
+  private static MethodHandle find(
+      Class<?> bridge, String name, Class<?> returnType, Class<?>... 
parameterTypes)
+      throws ReflectiveOperationException {
+    return MethodHandles.publicLookup()
+      .findStatic(bridge, name, MethodType.methodType(returnType, 
parameterTypes));
+  }
+
+  /**
+   * Loads the library at the given path into a new copy of {@link 
NativeBridge}.
+   */
+  static NativeLibrary load(String path) throws Throwable {
+    Class<?> bridge = new 
BridgeClassLoader(NativeBridge.class.getClassLoader())
+      .loadClass(BRIDGE_CLASS);
+    Method load = bridge.getDeclaredMethod("load", String.class);
+    load.setAccessible(true);

Review Comment:
   Added a comment in 9ac5a49. The copy has to call `System.load` itself, 
because a library belongs to the class loader of the class that loads it. 
`NativeBridge.load` is package-private so it stays out of the public interface, 
which is why it's called reflectively. This keeps working as long as Spark runs 
from the class path: classes there are in unnamed modules, whose packages are 
all open, so deep reflection doesn't depend on `--add-opens`.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSource.scala:
##########
@@ -0,0 +1,655 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.execution.datasources.v2.ffi
+
+import java.lang.ref.{Cleaner, WeakReference}
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicLong
+
+import scala.jdk.CollectionConverters._
+import scala.util.control.NonFatal
+
+import org.apache.arrow.c.{ArrowArray, ArrowArrayStream, ArrowSchema, Data}
+import org.apache.arrow.vector.{FieldVector, VectorSchemaRoot}
+import org.apache.arrow.vector.ipc.ArrowReader
+import org.apache.arrow.vector.types.pojo.Field
+
+import org.apache.spark.{SparkException, SparkThrowable, 
SparkUnsupportedOperationException}
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.connector.expressions.filter.Predicate
+import org.apache.spark.sql.connector.read.{InputPartition, PartitionReader}
+import org.apache.spark.sql.connector.write.{DataWriter, WriterCommitMessage}
+import org.apache.spark.sql.errors.{QueryCompilationErrors, 
QueryExecutionErrors}
+import 
org.apache.spark.sql.execution.datasources.v2.columnar.{ColumnarDataSource, 
ColumnarReader, ColumnarStreamReader, ColumnarStreamWriter, ColumnarWriter}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.{DataType, StructType}
+import org.apache.spark.sql.util.{ArrowUtils, CaseInsensitiveStringMap}
+import org.apache.spark.sql.vectorized.{ArrowColumnVector, ColumnarBatch, 
ColumnVector}
+import org.apache.spark.util.Utils
+
+/**
+ * A [[ColumnarDataSource]] implemented by a native library, through
+ * `org.apache.spark.sql.datasource.NativeBridge`.
+ *
+ * @param location where the library is on the driver. The readers and the 
writers distribute it
+ *                 to the executors of the active session when they are 
created.
+ * @param name the name of the data source, as listed in the manifest of its 
package or in lower
+ *             case for an installed library
+ */
+class NativeDataSource(
+    location: NativeLibraryLocation,
+    name: String,
+    options: CaseInsensitiveStringMap)
+  extends ColumnarDataSource {
+  import NativeCalls._
+
+  override def schema(): StructType = {
+    val library = NativeLibraries.get(location)
+    withDataSource(library, "infer the schema of") { dataSource =>
+      call(name, "infer the schema of") {
+        optional(NativeArrow.importSchema(address => 
library.schema(dataSource, address)))
+      }.getOrElse(throw 
QueryCompilationErrors.dataSchemaNotSpecifiedError(name))
+    }
+  }
+
+  override def reader(schema: StructType): ColumnarReader = {
+    val executorLocation = NativeDataSourceRegistry.distribute(location)
+    val library = NativeLibraries.get(location)
+    val handle = create(library, "plan a scan of", 
"DATA_SOURCE_BATCH_SCAN_NOT_SUPPORTED") {
+      dataSource =>
+        NativeArrow.exportSchema(schema)(address => 
library.createReader(dataSource, address))
+    }
+    new NativeDataSourceReader(
+      executorLocation, name, schema, new NativeHandle(handle, 
library.closeReader))
+  }
+
+  override def streamReader(schema: StructType): ColumnarStreamReader = {
+    val executorLocation = NativeDataSourceRegistry.distribute(location)
+    val library = NativeLibraries.get(location)
+    val handle = create(library, "plan a scan of", 
"DATA_SOURCE_MICRO_BATCH_SCAN_NOT_SUPPORTED") {
+      dataSource =>
+        NativeArrow.exportSchema(schema)(address => 
library.createStreamReader(dataSource, address))
+    }
+    new NativeDataSourceStreamReader(
+      executorLocation, name, schema, new NativeHandle(handle, 
library.closeStreamReader))
+  }
+
+  override def writer(schema: StructType, overwrite: Boolean): ColumnarWriter 
= {
+    val executorLocation = NativeDataSourceRegistry.distribute(location)
+    val library = NativeLibraries.get(location)
+    val handle = new NativeHandle(
+      create(library, "plan a write to", 
"DATA_SOURCE_BATCH_WRITE_NOT_SUPPORTED") { dataSource =>
+        NativeArrow.exportSchema(schema) { address =>
+          library.createWriter(dataSource, address, overwrite)
+        }
+      },
+      library.closeWriter)
+    new NativeDataSourceWriter(executorLocation, name, handle, 
serializeWriter(library, handle))
+  }
+
+  override def streamWriter(
+      schema: StructType,
+      overwrite: Boolean,
+      queryId: String): ColumnarStreamWriter = {
+    val executorLocation = NativeDataSourceRegistry.distribute(location)
+    val library = NativeLibraries.get(location)
+    val handle = new NativeHandle(
+      create(library, "plan a write to", 
"DATA_SOURCE_STREAMING_WRITE_NOT_SUPPORTED") {
+        dataSource =>
+          NativeArrow.exportSchema(schema) { address =>
+            library.createStreamWriter(dataSource, address, overwrite)
+          }
+      },
+      library.closeWriter)
+    val writer = new NativeDataSourceStreamWriter(
+      executorLocation, name, handle, serializeWriter(library, handle))
+    NativeStreamWriters.register(queryId, handle)
+    writer
+  }
+
+  /** Runs `f` with a new native data source, which is closed afterwards. */
+  private def withDataSource[T](library: NativeLibrary, action: String)(f: 
Long => T): T = {
+    // The keys of a case-insensitive map are in lower case.
+    val entries = options.entrySet().asScala.toArray
+    val dataSource = call(name, "create") {
+      library.createDataSource(name, entries.map(_.getKey), 
entries.map(_.getValue))
+    }
+    Utils.tryWithSafeFinally(f(dataSource)) {
+      call(name, action)(library.closeDataSource(dataSource))
+    }
+  }
+
+  /** Creates a reader or a writer with `f`, which fails if the library does 
not implement it. */
+  private def create(library: NativeLibrary, action: String, unsupportedError: 
String)(
+      f: Long => Long): Long = {
+    withDataSource(library, action) { dataSource =>
+      call(name, action)(optional(f(dataSource))).getOrElse {
+        throw new SparkUnsupportedOperationException(unsupportedError, 
Map("description" -> name))
+      }
+    }
+  }
+
+  private def serializeWriter(library: NativeLibrary, handle: NativeHandle): 
Array[Byte] = {
+    try {
+      bytesOrEmpty(call(name, "plan a write 
to")(optional(library.serializeWriter(handle.get))))
+    } catch {
+      case e: Throwable =>
+        handle.close()
+        throw e
+    }
+  }
+}
+
+/** Reads a native data source for a batch scan. */
+class NativeDataSourceReader(
+    location: NativeLibraryLocation,
+    name: String,
+    schema: StructType,
+    @transient private val handle: NativeHandle)
+  extends ColumnarReader {
+  import NativeCalls._
+
+  // Set on the driver when the partitions are planned, and serialized to the 
executors.
+  private var readerState: Array[Byte] = Array.emptyByteArray
+  private var readSchema: StructType = schema
+  @transient private var plannedPartitions: Array[InputPartition] = _
+
+  @transient private lazy val library = NativeLibraries.get(location)
+
+  override def pushPredicates(predicates: Array[Predicate]): Array[Predicate] 
= {
+    // Only the predicates that can be expressed in JSON are passed to the 
library.
+    val encoded = predicates.zipWithIndex.flatMap { case (predicate, i) =>
+      NativePredicates.toJson(predicate, schema).map(_ -> i)
+    }
+    val pushed = if (encoded.isEmpty) {
+      Set.empty[Int]
+    } else {
+      call(name, "plan a scan of") {
+        optional(library.pushPredicates(handle.get, encoded.map(_._1)))
+      } match {
+        case Some(flags) if flags != null && flags.length == encoded.length =>
+          encoded.map(_._2).zip(flags).collect { case (i, true) => i }.toSet
+        case Some(flags) =>
+          throw error(name, "plan a scan of", s"pushPredicates returned " +
+            s"${Option(flags).map(_.length).getOrElse(0)} values for 
${encoded.length} predicates.")
+        case None => Set.empty[Int]
+      }
+    }
+    predicates.zipWithIndex.collect { case (predicate, i) if 
!pushed.contains(i) => predicate }
+  }
+
+  override def pushLimit(limit: Int): Boolean = {
+    call(name, "plan a scan of")(optional(library.pushLimit(handle.get, 
limit))).getOrElse(false)
+  }
+
+  override def pruneColumns(requiredSchema: StructType): Boolean = {
+    val pruned = call(name, "plan a scan of") {
+      optional(library.pruneColumns(handle.get, requiredSchema.fieldNames))
+    }.getOrElse(false)
+    if (pruned) {
+      readSchema = requiredSchema
+    }
+    pruned
+  }
+
+  override def partitions(): Array[InputPartition] = synchronized {
+    if (plannedPartitions == null) {
+      try {
+        val partitions = call(name, "plan a scan 
of")(library.partitions(handle.get))
+        readerState = bytesOrEmpty(
+          call(name, "plan a scan 
of")(optional(library.serializeReader(handle.get))))
+        plannedPartitions = NativeInputPartition.from(partitions)
+      } finally {
+        // The executors only need the serialized state.
+        handle.close()
+      }
+    }
+    plannedPartitions
+  }
+
+  override def read(partition: InputPartition): PartitionReader[ColumnarBatch] 
= {
+    new NativePartitionReader(
+      location, name, readerState, 
partition.asInstanceOf[NativeInputPartition].bytes, readSchema)
+  }
+}
+
+/** Reads a native data source for a micro-batch streaming scan. */
+class NativeDataSourceStreamReader(
+    location: NativeLibraryLocation,
+    name: String,
+    schema: StructType,
+    @transient private val handle: NativeHandle)
+  extends ColumnarStreamReader {
+  import NativeCalls._
+
+  // Set on the driver whenever the partitions of a micro-batch are planned, 
and serialized to
+  // the executors with the partitions of that micro-batch.
+  private var readerState: Array[Byte] = Array.emptyByteArray
+
+  @transient private lazy val library = NativeLibraries.get(location)
+
+  override def initialOffset(): String = {
+    call(name, "plan a scan of")(library.initialOffset(handle.get))
+  }
+
+  override def latestOffset(): String = {
+    call(name, "plan a scan of")(library.latestOffset(handle.get))
+  }
+
+  override def partitions(start: String, end: String): Array[InputPartition] = 
{
+    val partitions = call(name, "plan a scan of") {
+      library.streamPartitions(handle.get, start, end)
+    }
+    readerState = bytesOrEmpty(
+      call(name, "plan a scan 
of")(optional(library.serializeStreamReader(handle.get))))
+    NativeInputPartition.from(partitions)
+  }
+
+  override def read(partition: InputPartition): PartitionReader[ColumnarBatch] 
= {
+    new NativePartitionReader(
+      location, name, readerState, 
partition.asInstanceOf[NativeInputPartition].bytes, schema)
+  }
+
+  override def commit(end: String): Unit = {
+    call(name, "commit an offset 
of")(optional(library.commitOffset(handle.get, end)))
+  }
+
+  override def stop(): Unit = handle.close()
+}
+
+/** Writes to a native data source for a batch write. */
+class NativeDataSourceWriter(
+    location: NativeLibraryLocation,
+    name: String,
+    @transient private val handle: NativeHandle,
+    writerState: Array[Byte])
+  extends ColumnarWriter {
+  import NativeCalls._
+
+  @transient private lazy val library = NativeLibraries.get(location)
+
+  override def createWriter(partitionId: Int, taskId: Long): 
DataWriter[ColumnarBatch] = {
+    new NativeDataWriter(location, name, writerState, partitionId, taskId, 
epochId = -1L)
+  }
+
+  override def commit(messages: Array[WriterCommitMessage]): Unit = {
+    call(name, "commit a write to") {
+      library.commit(handle.get, -1L, 
NativeWriterCommitMessage.toBytes(messages))
+    }
+    handle.close()
+  }
+
+  override def abort(messages: Array[WriterCommitMessage]): Unit = {
+    Utils.tryWithSafeFinally {
+      call(name, "abort a write to") {
+        library.abort(handle.get, -1L, 
NativeWriterCommitMessage.toBytes(messages))
+      }
+    }(handle.close())
+  }
+}
+
+/**
+ * Writes to a native data source for a streaming write. Its handle is closed 
when the streaming
+ * query creates its next stream writer, see [[NativeStreamWriters]].
+ */
+class NativeDataSourceStreamWriter(
+    location: NativeLibraryLocation,
+    name: String,
+    @transient private val handle: NativeHandle,
+    writerState: Array[Byte])
+  extends ColumnarStreamWriter {
+  import NativeCalls._
+
+  @transient private lazy val library = NativeLibraries.get(location)
+
+  override def createWriter(
+      partitionId: Int,
+      taskId: Long,
+      epochId: Long): DataWriter[ColumnarBatch] = {
+    new NativeDataWriter(location, name, writerState, partitionId, taskId, 
epochId)
+  }
+
+  override def commit(epochId: Long, messages: Array[WriterCommitMessage]): 
Unit = {
+    call(name, "commit a write to") {
+      library.commit(handle.get, epochId, 
NativeWriterCommitMessage.toBytes(messages))
+    }
+  }
+
+  override def abort(epochId: Long, messages: Array[WriterCommitMessage]): 
Unit = {
+    call(name, "abort a write to") {
+      library.abort(handle.get, epochId, 
NativeWriterCommitMessage.toBytes(messages))
+    }
+  }
+}
+
+/** A partition planned by a native library. */
+case class NativeInputPartition(index: Int, bytes: Array[Byte]) extends 
InputPartition
+
+object NativeInputPartition {
+  def from(partitions: Array[Array[Byte]]): Array[InputPartition] = {
+    Option(partitions).getOrElse(Array.empty).zipWithIndex.map { case (bytes, 
i) =>
+      NativeInputPartition(i, Option(bytes).getOrElse(Array.emptyByteArray))
+    }
+  }
+}
+
+/** The message returned by the data writer of a task of a native library. */
+case class NativeWriterCommitMessage(bytes: Array[Byte]) extends 
WriterCommitMessage
+
+object NativeWriterCommitMessage {
+  def toBytes(messages: Array[WriterCommitMessage]): Array[Array[Byte]] = 
messages.map {
+    case message: NativeWriterCommitMessage => message.bytes
+    case null => null
+    case other =>
+      throw SparkException.internalError(s"Unexpected commit message: 
${other.getClass.getName}")
+  }
+}
+
+/**
+ * Reads a partition of a native data source: the Arrow stream exported by the 
library is
+ * imported into Arrow vectors that back the returned batch.
+ */
+class NativePartitionReader(
+    location: NativeLibraryLocation,
+    name: String,
+    readerState: Array[Byte],
+    partition: Array[Byte],
+    schema: StructType)
+  extends PartitionReader[ColumnarBatch] {
+  import NativeCalls._
+
+  private val allocator =
+    ArrowUtils.rootAllocator.newChildAllocator(s"native data source $name", 0, 
Long.MaxValue)
+  private var reader: ArrowReader = _
+  private var root: VectorSchemaRoot = _
+  private var batch: ColumnarBatch = _
+
+  try {
+    reader = openStream()
+    root = call(name, "read")(reader.getVectorSchemaRoot)
+    checkSchema()
+    batch = new ColumnarBatch(
+      root.getFieldVectors.asScala.map(new ArrowColumnVector(_): 
ColumnVector).toArray)
+  } catch {
+    case e: Throwable =>
+      close()
+      throw e
+  }
+
+  private def openStream(): ArrowReader = {
+    val library = NativeLibraries.get(location)
+    val stream = ArrowArrayStream.allocateNew(allocator)
+    try {
+      call(name, "read")(optional(library.read(readerState, partition, 
stream.memoryAddress())))
+        .getOrElse {
+          throw new SparkUnsupportedOperationException(
+            "DATA_SOURCE_BATCH_SCAN_NOT_SUPPORTED", Map("description" -> name))
+        }
+      Data.importArrayStream(allocator, stream)
+    } catch {
+      case e: Throwable =>
+        stream.release()
+        throw e
+    } finally {
+      stream.close()
+    }
+  }
+
+  private def checkSchema(): Unit = {
+    def isDictionaryEncoded(field: Field): Boolean = {
+      field.getDictionary != null || 
field.getChildren.asScala.exists(isDictionaryEncoded)
+    }
+    root.getSchema.getFields.asScala.find(isDictionaryEncoded).foreach { field 
=>
+      throw error(name, "read", s"The column ${field.getName} is 
dictionary-encoded, which " +
+        "is not supported.")
+    }
+    val actual = ArrowUtils.fromArrowSchema(root.getSchema)
+    val matches = actual.length == schema.length && actual.zip(schema).forall {
+      case (actualField, field) =>
+        actualField.name == field.name &&
+          DataType.equalsIgnoreNullability(actualField.dataType, 
field.dataType)

Review Comment:
   Good catch, fixed in 9ac5a49. `checkSchema` now compares against 
`fromArrowSchema(toArrowSchema(schema))`, the read schema as the library 
received it, and the error message shows that schema. The collation (and 
interval fields, UDTs) still applies to the data Spark reads. `read with a 
user-specified schema` now reads `id BIGINT, name STRING COLLATE UTF8_LCASE`, 
checks that `name = 'N1'` returns `n1`, and checks that the predicate isn't 
pushed down.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/columnar/ColumnarWrite.scala:
##########
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.execution.datasources.v2.columnar
+
+import scala.jdk.CollectionConverters._
+
+import org.apache.arrow.vector.VectorSchemaRoot
+
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.connector.metric.CustomTaskMetric
+import org.apache.spark.sql.connector.write._
+import 
org.apache.spark.sql.connector.write.streaming.{StreamingDataWriterFactory, 
StreamingWrite}
+import org.apache.spark.sql.execution.arrow.ArrowWriter
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.StructType
+import org.apache.spark.sql.util.ArrowUtils
+import org.apache.spark.sql.vectorized.{ArrowColumnVector, ColumnarBatch, 
ColumnVector}
+import org.apache.spark.util.Utils
+
+class ColumnarWriteBuilder(
+    shortName: String,
+    info: LogicalWriteInfo,
+    createDataSource: () => ColumnarDataSource)
+  extends WriteBuilder with SupportsTruncate {
+
+  private var overwrite = false
+
+  override def truncate(): WriteBuilder = {
+    overwrite = true
+    this
+  }
+
+  override def build(): Write = new ColumnarWrite(shortName, info, overwrite, 
createDataSource)
+}
+
+class ColumnarWrite(
+    shortName: String,
+    info: LogicalWriteInfo,
+    overwrite: Boolean,
+    createDataSource: () => ColumnarDataSource)
+  extends Write {
+
+  override def description(): String = shortName
+
+  override def toBatch: BatchWrite = {
+    new ColumnarBatchWrite(createDataSource().writer(info.schema(), 
overwrite), info.schema())
+  }
+
+  override def toStreaming: StreamingWrite = {
+    new ColumnarStreamingWrite(
+      createDataSource().streamWriter(info.schema(), overwrite, 
info.queryId()), info.schema())
+  }
+}
+
+class ColumnarBatchWrite(writer: ColumnarWriter, schema: StructType) extends 
BatchWrite {
+
+  override def createBatchWriterFactory(info: PhysicalWriteInfo): 
DataWriterFactory = {
+    new ColumnarDataWriterFactory(writer, ArrowBatchingOptions(schema))
+  }
+
+  override def commit(messages: Array[WriterCommitMessage]): Unit = 
writer.commit(messages)
+
+  override def abort(messages: Array[WriterCommitMessage]): Unit = 
writer.abort(messages)
+}
+
+class ColumnarStreamingWrite(writer: ColumnarStreamWriter, schema: StructType)
+  extends StreamingWrite {
+
+  override def createStreamingWriterFactory(
+      info: PhysicalWriteInfo): StreamingDataWriterFactory = {
+    new ColumnarStreamingDataWriterFactory(writer, 
ArrowBatchingOptions(schema))
+  }
+
+  override def commit(epochId: Long, messages: Array[WriterCommitMessage]): 
Unit = {
+    writer.commit(epochId, messages)
+  }
+
+  override def abort(epochId: Long, messages: Array[WriterCommitMessage]): 
Unit = {
+    writer.abort(epochId, messages)
+  }
+}
+
+class ColumnarDataWriterFactory(writer: ColumnarWriter, options: 
ArrowBatchingOptions)
+  extends DataWriterFactory {
+  override def createWriter(partitionId: Int, taskId: Long): 
DataWriter[InternalRow] = {
+    new ArrowBatchingDataWriter(writer.createWriter(partitionId, taskId), 
options)
+  }
+}
+
+class ColumnarStreamingDataWriterFactory(
+    writer: ColumnarStreamWriter,
+    options: ArrowBatchingOptions)
+  extends StreamingDataWriterFactory {
+  override def createWriter(
+      partitionId: Int,
+      taskId: Long,
+      epochId: Long): DataWriter[InternalRow] = {
+    new ArrowBatchingDataWriter(writer.createWriter(partitionId, taskId, 
epochId), options)
+  }
+}
+
+/**
+ * How [[ArrowBatchingDataWriter]] converts rows to Arrow, captured on the 
driver from the
+ * session configuration.
+ */
+case class ArrowBatchingOptions(schema: StructType, timeZoneId: String, 
maxRecordsPerBatch: Int)
+
+object ArrowBatchingOptions {
+  def apply(schema: StructType): ArrowBatchingOptions = {
+    val conf = SQLConf.get
+    val maxRecords = conf.arrowMaxRecordsPerBatch
+    ArrowBatchingOptions(
+      schema, conf.sessionLocalTimeZone, if (maxRecords > 0) maxRecords else 
Int.MaxValue)
+  }
+}
+
+/**
+ * Converts the rows of a write task to Arrow-backed [[ColumnarBatch]]es for 
the [[DataWriter]]
+ * of a [[ColumnarDataSource]]. A new Arrow vector schema root is created for 
each batch, so a
+ * native writer that took ownership of the data of a batch can keep it after 
the call.
+ */
+class ArrowBatchingDataWriter(delegate: DataWriter[ColumnarBatch], options: 
ArrowBatchingOptions)
+  extends DataWriter[InternalRow] {
+
+  private val arrowSchema = ArrowUtils.toArrowSchema(
+    options.schema, options.timeZoneId, errorOnDuplicatedFieldNames = true, 
largeVarTypes = false)
+  private val allocator =
+    ArrowUtils.rootAllocator.newChildAllocator("columnar data source writer", 
0, Long.MaxValue)
+  private var root: VectorSchemaRoot = _
+  private var arrowWriter: ArrowWriter = _
+  private var numRows = 0
+
+  override def write(record: InternalRow): Unit = {
+    if (root == null) {
+      root = VectorSchemaRoot.create(arrowSchema, allocator)
+      arrowWriter = ArrowWriter.create(root)
+    }
+    arrowWriter.write(record)
+    numRows += 1
+    if (numRows >= options.maxRecordsPerBatch) {

Review Comment:
   Fixed in 9ac5a49. `ArrowBatchingOptions` now also carries 
`spark.sql.execution.arrow.maxBytesPerBatch`. A batch ends when it reaches the 
row limit or, after a row, holds at least that many bytes 
(`arrowWriter.sizeInBytes()`), like `PythonArrowInput`. It also skips computing 
the size when the limit is `Int.MaxValue`, as Python UDFs do. The 
`NativeBridge` Javadoc mentions both limits. New test: with 
`maxBytesPerBatch=4096` and rows of about 1 KB, ten rows are written as batches 
of `[5, 5]`. A single row that alone exceeds the 2 GB offset limit would still 
fail, as it would in any Arrow path with `largeVarTypes = false`.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceRegistry.scala:
##########
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.execution.datasources.v2.ffi
+
+import java.io.File
+import java.nio.file.Paths
+import java.util.Locale
+
+import org.apache.spark.{SparkEnv, SparkFiles}
+import org.apache.spark.internal.Logging
+import org.apache.spark.internal.LogKeys.PATH
+import org.apache.spark.sql.Artifact
+import org.apache.spark.sql.classic.SparkSession
+import org.apache.spark.sql.datasource.NativeBridge
+import org.apache.spark.sql.errors.{QueryCompilationErrors, 
QueryExecutionErrors}
+import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
+import org.apache.spark.util.Utils
+
+/**
+ * Finds the native data sources on the driver. Like the Python data sources 
installed in the
+ * Python path, the native data source libraries installed in the native 
library path are found
+ * automatically, by their file name: the library `spark_datasource_<name>`, 
such as
+ * `libspark_datasource_my_source.so` on Linux, implements the data source 
`<name>`. The active
+ * session can also add packages with `spark.addArtifact`, or with
+ * `spark.sql.dataSource.native.paths`, which take precedence over the 
installed libraries.
+ */
+object NativeDataSourceRegistry extends Logging {
+
+  /** The prefix of the names of the installed native data source libraries. */
+  val LIBRARY_NAME_PREFIX = "spark_datasource_"
+
+  // The result of the last lookup by `DataSource.lookupDataSource` on this 
thread, if it found a
+  // native data source, by the name it looked up. The provider that the 
caller creates next for
+  // that name takes it, so that it does not look the name up again: see 
[[takeLookup]]. Each
+  // lookup clears it first, so a provider never takes the result of an 
earlier lookup.
+  private val lastLookup = new ThreadLocal[(String, (String, 
NativeLibraryLocation))]
+
+  /**
+   * Finds the package or the installed library that provides the native data 
source with the
+   * given name, which is case-insensitive. Returns the name of the data 
source, as listed in the
+   * manifest of the package or in lower case for an installed library, and 
where its library is.
+   */
+  def lookup(name: String, conf: SQLConf): Option[(String, 
NativeLibraryLocation)] = {
+    if (!conf.getConf(StaticSQLConf.NATIVE_DATA_SOURCE_ENABLED)) {
+      return None
+    }
+    val (invalid, valid) = (configuredPackageFiles(conf) ++ 
artifactPackageFiles())
+      .map(NativeDataSourcePackage.tryRead).partitionMap(identity)
+    // A package of a version of the interface that Spark does not support is 
unusable too.
+    val (unsupported, usable) = valid.partitionMap { pkg =>
+      if (pkg.manifest.abiVersion == NativeBridge.ABI_VERSION) Right(pkg) else 
Left(pkg)
+    }
+    lookupPackage(name, usable)
+      .orElse(lookupInstalledLibrary(name))
+      .orElse {
+        // An unusable package is only reported when no other package or 
installed library
+        // provides the data source: a stale copy does not hide a valid one. 
An invalid package
+        // whose manifest cannot be read is only logged, when it is read.
+        
unsupported.find(_.manifest.dataSources.exists(_.equalsIgnoreCase(name))).foreach
 { pkg =>
+          throw QueryExecutionErrors.invalidNativeDataSourcePackageError(
+            pkg.path,
+            "UNSUPPORTED_ABI_VERSION",
+            Map(
+              "version" -> pkg.manifest.abiVersion.toString,
+              "supported" -> NativeBridge.ABI_VERSION.toString))
+        }
+        invalid.find(_.dataSources.exists(_.equalsIgnoreCase(name))).foreach(p 
=> throw p.error)
+        None
+      }
+  }
+
+  /**
+   * Like [[lookup]], and keeps the result for the provider that the caller 
creates next for the
+   * given name, which takes it with [[takeLookup]].
+   */
+  def lookupForProvider(name: String, conf: SQLConf): Option[(String, 
NativeLibraryLocation)] = {
+    val result = lookup(name, conf)
+    result.foreach(found => lastLookup.set(name -> found))
+    result
+  }
+
+  /**
+   * Takes the result of the last [[lookupForProvider]] on this thread, if it 
was for the given
+   * name, and forgets it.
+   */
+  def takeLookup(name: String): Option[(String, NativeLibraryLocation)] = {
+    val last = Option(lastLookup.get())
+    lastLookup.remove()
+    last.collect { case (lookedUp, found) if lookedUp.equalsIgnoreCase(name) 
=> found }
+  }
+
+  /** Forgets the result of the last [[lookupForProvider]] on this thread. */
+  def clearLookup(): Unit = lastLookup.remove()
+
+  private def lookupPackage(
+      name: String,
+      packages: Seq[NativeDataSourcePackage]): Option[(String, 
NativeDataSourcePackage)] = {
+    val found = packages
+      .flatMap(pkg => 
pkg.manifest.dataSources.find(_.equalsIgnoreCase(name)).map(_ -> pkg))
+      // The same package can be found more than once, for example when it is 
in a configured
+      // directory and was also added as an artifact.
+      .groupBy(_._2.sha256).values.map(_.head).toSeq
+    found match {
+      case Seq() => None
+      case Seq(result) => Some(result)
+      case _ =>
+        throw QueryCompilationErrors.nativeDataSourcePackageConflictError(
+          name, found.map(_._2.path).sorted)
+    }
+  }
+
+  private def lookupInstalledLibrary(name: String): Option[(String, 
InstalledNativeLibrary)] = {
+    // Other names, such as class names, are not the names of libraries.
+    if (!name.matches("[A-Za-z0-9_]+")) {
+      return None
+    }
+    val dataSource = name.toLowerCase(Locale.ROOT)
+    findInstalledLibrary(libraryFileName(dataSource))
+      .map(file => dataSource -> InstalledNativeLibrary(file.getPath))
+  }
+
+  /** Finds the library with the given file name in the native library path of 
this node. */
+  private[ffi] def findInstalledLibrary(fileName: String): Option[File] = {
+    librarySearchPath(System.getProperty("java.library.path"), 
System.getenv("PATH"))
+      .map(new File(_, fileName))
+      .find(_.isFile)
+  }
+
+  /**
+   * The file name of the installed library of the native data source with the 
given name, such
+   * as `libspark_datasource_my_source.so` on Linux, 
`libspark_datasource_my_source.dylib` on
+   * macOS and `spark_datasource_my_source.dll` on Windows.
+   */
+  def libraryFileName(name: String): String = {
+    System.mapLibraryName(LIBRARY_NAME_PREFIX + name.toLowerCase(Locale.ROOT))
+  }
+
+  /**
+   * The native library path: the directories where native data source 
libraries are installed,
+   * in the order they are searched. They are the directories of 
`java.library.path`, where the
+   * JVM finds native libraries, which include `LD_LIBRARY_PATH` on Linux, 
`DYLD_LIBRARY_PATH` on
+   * macOS and `PATH` on Windows, and then the `lib` directory of each 
installation prefix in
+   * `PATH`, such as `/usr/local/lib` for `/usr/local/bin`. Relative 
directories are ignored.
+   */
+  private[ffi] def librarySearchPath(javaLibraryPath: String, path: String): 
Seq[File] = {
+    def directories(value: String): Seq[File] = {
+      Option(value).toSeq.flatMap(_.split(File.pathSeparator)).map(new 
File(_)).filter(_.isAbsolute)
+    }
+    val prefixLibraries = directories(path)
+      .filter(_.getName == "bin")
+      .flatMap(bin => Option(bin.getParentFile))
+      .map(new File(_, "lib"))
+    (directories(javaLibraryPath) ++ prefixLibraries).distinct
+  }
+
+  /**
+   * Makes the library available to the executors of the active session, which 
runs the jobs of
+   * the scan or the write: for a streaming query, it is the session of the 
query, a clone of the
+   * session that started it. Returns where the executors find it. An 
installed library is already
+   * installed on every node.
+   */
+  def distribute(location: NativeLibraryLocation): NativeLibraryLocation = 
location match {
+    case pkg: NativeDataSourcePackage =>
+      SparkSession.getActiveSession match {
+        // In local mode, the executors run in the driver and read the package 
where it is.
+        case Some(session) if !session.sparkContext.isLocal => 
addToArtifacts(session, pkg)
+        case _ => pkg
+      }
+    case installed: InstalledNativeLibrary => installed
+  }
+
+  /** Adds the package as an artifact of the session, unless it already is 
one. */
+  private[ffi] def addToArtifacts(
+      session: SparkSession,
+      pkg: NativeDataSourcePackage): NativeDataSourcePackage = {
+    val artifactManager = session.artifactManager
+    val (added, artifactUUID) = artifactManager.getNativeDataSourcePackages
+    // The executors find the package by its file name, so the session cannot 
have another
+    // package with the same file name.
+    added.find(_.getName == pkg.fileName) match {
+      case Some(file) =>
+        val sha256 = 
NativeDataSourcePackage.tryRead(file).map(_.sha256).getOrElse("")
+        if (sha256 != pkg.sha256) {
+          throw 
QueryExecutionErrors.nativeDataSourcePackageFileNameConflictError(
+            pkg.path, pkg.fileName)
+        }
+      case None =>
+        artifactManager.addLocalArtifacts(Artifact.newFileArtifact(
+          Paths.get(pkg.fileName), new 
Artifact.LocalFile(Paths.get(pkg.path))) :: Nil)
+    }
+    pkg.copy(artifactUUID = artifactUUID)
+  }
+
+  /**
+   * The local copies, on an executor, of a package distributed with 
[[distribute]], in the order
+   * to check them. The files of an isolated session are stored in a directory 
named after the
+   * UUID of the session; the copies of other sessions are checked last, 
because the executors
+   * check the checksum of the package anyway.
+   */
+  def distributedCopies(pkg: NativeDataSourcePackage): Seq[File] = {
+    if (SparkEnv.get == null) {
+      Nil
+    } else {
+      val root = new File(SparkFiles.getRootDirectory())
+      val sessionCopies = Option(root.listFiles()).toSeq.flatten
+        .filter(_.isDirectory)
+        .sortBy(_.getName)
+        .map(new File(_, pkg.fileName))
+      (pkg.artifactUUID.map(uuid => new File(new File(root, uuid), 
pkg.fileName)).toSeq ++
+        Seq(new File(root, pkg.fileName)) ++ sessionCopies).distinct
+    }
+  }
+
+  private def configuredPackageFiles(conf: SQLConf): Seq[File] = {
+    conf.getConf(SQLConf.NATIVE_DATA_SOURCE_PATHS).flatMap { path =>
+      val uri = Utils.resolveURI(path)
+      val file = new File(uri.getPath)
+      if (uri.getScheme != null && uri.getScheme != "file") {
+        logWarning(log"Ignoring the native data source path ${MDC(PATH, 
path)}: only local " +
+          log"paths are supported. Add remote packages with spark.addArtifact 
instead.")
+        Nil
+      } else if (file.isDirectory) {
+        packageFilesIn(file)
+      } else if (file.isFile) {
+        Seq(file)
+      } else {
+        logWarning(log"The native data source path ${MDC(PATH, path)} does not 
exist.")
+        Nil
+      }
+    }
+  }
+
+  private def packageFilesIn(dir: File): Seq[File] = {
+    dir.listFiles()
+      .filter(f => f.isFile && 
f.getName.endsWith(NativeDataSourcePackage.FILE_EXTENSION))
+      .sortBy(_.getName)
+      .toSeq
+  }
+
+  private def artifactPackageFiles(): Seq[File] = {
+    
SparkSession.getActiveSession.toSeq.flatMap(_.artifactManager.getNativeDataSourcePackages._1)

Review Comment:
   Thanks for the concrete repro, fixed in 9ac5a49 as you suggested. Classic 
`DataFrameWriter.runCommand` now takes the command by name and builds it under 
`session.withActive`, which covers `save`, `insertInto` and `saveAsTable`. 
`DataStreamWriter.startInternal` resolves the sink (lookup, provider and 
`getTable`) under `ds.sparkSession.withActive`, via a new `resolveSink`. That 
also fixes the same pre-existing problem for Python data sources, whose lookup 
also uses the active session. Connect already builds these commands under the 
session's `withActive`. The new test `write with a session that is not the 
active one` adds the package to a non-active session, then does a batch 
`save()` and a streaming `start()`. With the old `runCommand` it fails with 
`DATA_SOURCE_NOT_FOUND`, as in your repro.



-- 
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