sandip-db commented on code in PR #59435:
URL: https://github.com/apache/spark/pull/59435#discussion_r4433374281


##########
core/src/main/scala/org/apache/spark/io/HadoopCodecStreams.scala:
##########
@@ -38,8 +40,49 @@ import org.apache.spark.io.{CompressionCodec => 
SparkCompressionCodec}
 object HadoopCodecStreams {
   private val ZSTD_EXTENSIONS = Seq(".zstd", ".zst")
 
-  // get codec based on file name extension
+  /**
+   * Hadoop `Configuration` key carrying the read-side compression codec 
explicitly requested via
+   * the `decompression` data source option. The value is a full codec class 
name, an
+   * empty string to force "no compression" (read the file as-is), or absent 
to infer the codec
+   * from the file name extension. Set by the file format / scan read paths 
and consumed by
+   * [[getDecompressionCodec]] so it uniformly overrides extension-based 
detection for line-based,
+   * multi-line, whole-text, and schema-inference reads.
+   *
+   * This is an internal key, deliberately outside the `spark.*` namespace so 
it is not mistaken for
+   * a user-facing configuration. The read paths always overwrite or unset it, 
so a value inherited
+   * from the session or Hadoop configuration is never honored.
+   */
+  val DECOMPRESSION_CODEC_CONF = 
"org.apache.spark.sql.execution.datasources.decompressionCodec"
+
+  /**
+   * Returns the codec to decompress `file` with. When a read compression 
option has been set (see
+   * [[DECOMPRESSION_CODEC_CONF]]) it takes precedence over the file name 
extension: a codec class
+   * name selects that codec, and an empty value means the file is read as-is. 
Otherwise the codec
+   * is inferred from the file name extension.
+   */
   def getDecompressionCodec(
+    config: Configuration,
+    file: Path): Option[CompressionCodec] = {
+    Option(config.get(DECOMPRESSION_CODEC_CONF)) match {
+      case Some(codecClassName) if codecClassName.nonEmpty =>
+        // The compression codec was explicitly specified via the read option, 
so use it instead of
+        // inferring from the file name extension. Resolve it the same way the 
write path does
+        // (see CodecStreams.getCompressionCodec): load the class and 
instantiate it, so any
+        // loadable CompressionCodec is supported, not just those registered 
with the factory.
+        val codecClass = config.getClassByName(codecClassName)
+        Some(ReflectionUtils.newInstance(codecClass, 
config).asInstanceOf[CompressionCodec])
+      case Some(_) =>
+        // The read option was explicitly set to `none`/`uncompressed`: read 
the file as-is,
+        // ignoring any compression-related file name extension.

Review Comment:
   Yes. `none`/`uncompressed` is stored as an empty string. I made that 
explicit: it's now matched as `case Some("")`, before the class-name case.
   



##########
core/src/main/scala/org/apache/spark/io/HadoopCodecStreams.scala:
##########
@@ -56,12 +99,26 @@ object HadoopCodecStreams {
   }
 
   def createZstdInputStream(
+    file: Path,
+    inputStream: InputStream): Option[InputStream] = {
+    createZstdInputStream(new Configuration(false), file, inputStream)
+  }
+
+  def createZstdInputStream(
+    config: Configuration,

Review Comment:
   Done: I removed the 2-argument overload instead. It had no callers left. 
Both `createInputStream` and `HadoopLineRecordReader` pass the read's Hadoop 
conf, so no default conf is built anymore.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CompressionCodecs.scala:
##########
@@ -21,21 +21,46 @@ import java.util.Locale
 
 import org.apache.hadoop.conf.Configuration
 import org.apache.hadoop.io.SequenceFile.CompressionType
+import org.apache.hadoop.io.compress.{SplittableCompressionCodec, 
ZStandardCodec}
 
+import org.apache.spark.io.HadoopCodecStreams
 import org.apache.spark.sql.errors.QueryExecutionErrors
+import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.util.Utils
 
 object CompressionCodecs {
   private val shortCompressionCodecNames = HadoopCompressionCodec.values().map 
{ codec =>
     codec.lowerCaseName() -> 
Option(codec.getCompressionCodec).map(_.getClass.getName).orNull
   }.toMap
 
+  // Read-side short names extend the write-side set with Zstandard, which 
file-format writers do
+  // not support yet (see the TODO in `HadoopCompressionCodec`) but readers 
do, via Spark's
+  // built-in Zstandard codec when Hadoop lacks native support (see 
`HadoopCodecStreams`).
+  private val readShortCompressionCodecNames = shortCompressionCodecNames ++ 
Map(

Review Comment:
   Good idea, done: `zst` is now a read short name for `ZStandardCodec`



##########
core/src/main/scala/org/apache/spark/io/HadoopCodecStreams.scala:
##########
@@ -56,12 +99,26 @@ object HadoopCodecStreams {
   }
 
   def createZstdInputStream(
+    file: Path,
+    inputStream: InputStream): Option[InputStream] = {
+    createZstdInputStream(new Configuration(false), file, inputStream)
+  }
+
+  def createZstdInputStream(
+    config: Configuration,
     file: Path,
     inputStream: InputStream): Option[InputStream] = {
     val sparkConf = Option(SparkEnv.get).map(_.conf).getOrElse(new SparkConf)
     val fileName = file.getName.toLowerCase(Locale.ROOT)
 
-    val isOpt = if (ZSTD_EXTENSIONS.exists(fileName.endsWith)) {
+    // Use Spark's Zstandard codec when the read option explicitly requested a 
Zstandard codec (in
+    // which case the extension may not match), or, when no codec was forced, 
when the file name has
+    // a Zstandard extension. A forced non-Zstandard codec is never replaced 
based on the extension.
+    val useZstd = Option(config.get(DECOMPRESSION_CODEC_CONF)) match {
+      case Some(codecClassName) => codecClassName == 
classOf[ZStandardCodec].getName

Review Comment:
   You can: `decompression` accepts `zstd` and `zstandard` (and now `zst`) in 
any letter case, as well as codec class names. The short name is resolved to 
the codec class name when the option is parsed 
(`CompressionCodecs.getReadCodecClassName`), and this internal conf only ever 
holds that class name, so comparing class names here covers all of them. The 
decode tests read with each of these names. I added a comment here to make that 
clear.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/CompressionCodecs.scala:
##########
@@ -66,4 +91,42 @@ object CompressionCodecs {
       conf.set("mapreduce.map.output.compress", "false")
     }
   }
+
+  /**
+   * Records the codec requested via the `decompression` data source option 
into the Hadoop
+   * `Configuration`, so the file readers decompress with it instead of 
inferring the codec from
+   * the file name extension (see 
[[HadoopCodecStreams.getDecompressionCodec]]). Called on the
+   * driver while planning the read, so an unsupported setup fails before any 
task runs.
+   *
+   * @param decompressionCodec the parsed `decompression` option: `None` when 
absent (unset
+   *                           the conf so the codec is inferred from the 
extension),
+   *                           `Some(null)` for `none`/`uncompressed` (read 
the file as-is), or
+   *                           `Some(className)` to force that codec.
+   */
+  def setReadCodecConfiguration(
+      conf: Configuration,
+      decompressionCodec: Option[String]): Unit = decompressionCodec match {
+    case Some(codec) =>
+      // Hadoop's `LineRecordReader` infers the codec from the file name 
extension and cannot be
+      // told otherwise, so fail rather than silently ignore the option.
+      if (!SQLConf.get.hadoopLineRecordReaderEnabled) {
+        throw 
QueryExecutionErrors.decompressionCodecWithLegacyLineReaderError()
+      }
+      // An empty string encodes "no compression" (read as-is); a class name 
selects that codec.
+      conf.set(HadoopCodecStreams.DECOMPRESSION_CODEC_CONF, 
Option(codec).getOrElse(""))
+    case None =>
+      // Unset rather than leave untouched, so a value inherited from the 
session or Hadoop
+      // configuration cannot override the codec of a read that did not ask 
for it.
+      conf.unset(HadoopCodecStreams.DECOMPRESSION_CODEC_CONF)

Review Comment:
   I'd like to keep the unset. The key is internal (it is deliberately outside 
the `spark.*` namespace) and is only meant to carry the parsed read option to 
the readers, and honoring a value set globally would cause real problems:
   
   - **Splitting would disagree with decompression.** Splits are decided from 
the parsed `decompression` option, not from this conf. A global key that forces 
gzip would decompress every file as gzip while the file name extensions still 
decide splitting, so a large `.csv` file would be split and then fail with 
`Cannot seek in GzipCodec compressed stream`, or silently lose rows with 
`ignoreCorruptFiles`. This is the same split/decode mismatch as the earlier 
libsvm comment.
   - **It would skip the option's checks:** short-name validation, the legacy 
line reader error and the archive rejection only run when the option is set.
   - **It would apply to every text-based read in the session,** with no 
documented contract.
   
   If a session-wide default codec turns out to be useful, I think it should be 
a proper `spark.sql.*` conf that feeds the option, so it gets the same checks 
and split handling.



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