Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 5 additions & 19 deletions docs/sql-data-sources-binaryFile.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,50 +28,36 @@ It produces a DataFrame with the following columns and possibly partition column
* `length`: LongType
* `content`: BinaryType

It supports the following read option:
<table class="table">
<tr><th><b>Property Name</b></th><th><b>Default</b></th><th><b>Meaning</b></th></tr>
<tr>
<td><code>pathGlobFilter</code></td>
<td>none (accepts all)</td>
<td>
An optional glob pattern to only include files with paths matching the pattern.
The syntax follows <code>org.apache.hadoop.fs.GlobFilter</code>.
It does not change the behavior of partition discovery.
</td>
</tr>
</table>

To read whole binary files, you need to specify the data source `format` as `binaryFile`.
For example, the following code reads all PNG files from the input directory:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep the pathGlobFilter option in the example? It is actually important for the use case. Just mention pathGlobFilter is a global option.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I will revert this.

For example, the following code reads all the files from the input directory:

<div class="codetabs">
<div data-lang="scala" markdown="1">
{% highlight scala %}

spark.read.format("binaryFile").option("pathGlobFilter", "*.png").load("/path/to/data")
spark.read.format("binaryFile").load("/path/to/data")

{% endhighlight %}
</div>

<div data-lang="java" markdown="1">
{% highlight java %}

spark.read().format("binaryFile").option("pathGlobFilter", "*.png").load("/path/to/data");
spark.read().format("binaryFile").load("/path/to/data");

{% endhighlight %}
</div>
<div data-lang="python" markdown="1">
{% highlight python %}

spark.read.format("binaryFile").option("pathGlobFilter", "*.png").load("/path/to/data")
spark.read.format("binaryFile").load("/path/to/data")

{% endhighlight %}
</div>
<div data-lang="r" markdown="1">
{% highlight r %}

read.df("/path/to/data", source = "binaryFile", pathGlobFilter = "*.png")
read.df("/path/to/data", source = "binaryFile")

{% endhighlight %}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ class DataFrameReader private[sql](sparkSession: SparkSession) extends Logging {
* <ul>
* <li>`timeZone` (default session local timezone): sets the string that indicates a timezone
* to be used to parse timestamps in the JSON/CSV datasources or partition values.</li>
* <li>`pathGlobFilter`: an optional glob pattern to only include files with paths matching
* the pattern. The syntax follows <code>org.apache.hadoop.fs.GlobFilter</code>.
* It does not change the behavior of partition discovery.</li>
* </ul>
*
* @since 1.4.0
Expand Down Expand Up @@ -135,6 +138,9 @@ class DataFrameReader private[sql](sparkSession: SparkSession) extends Logging {
* <ul>
* <li>`timeZone` (default session local timezone): sets the string that indicates a timezone
* to be used to parse timestamps in the JSON/CSV datasources or partition values.</li>
* <li>`pathGlobFilter`: an optional glob pattern to only include files with paths matching
* the pattern. The syntax follows <code>org.apache.hadoop.fs.GlobFilter</code>.
* It does not change the behavior of partition discovery.</li>
* </ul>
*
* @since 1.4.0
Expand All @@ -151,6 +157,9 @@ class DataFrameReader private[sql](sparkSession: SparkSession) extends Logging {
* <ul>
* <li>`timeZone` (default session local timezone): sets the string that indicates a timezone
* to be used to parse timestamps in the JSON/CSV datasources or partition values.</li>
* <li>`pathGlobFilter`: an optional glob pattern to only include files with paths matching
* the pattern. The syntax follows <code>org.apache.hadoop.fs.GlobFilter</code>.
* It does not change the behavior of partition discovery.</li>
* </ul>
*
* @since 1.4.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,8 @@ case class DataSource(
sparkSession.sessionState.newHadoopConf(),
sparkSession.sessionState.conf) =>
val basePath = new Path((caseInsensitiveOptions.get("path").toSeq ++ paths).head)
val fileCatalog = new MetadataLogFileIndex(sparkSession, basePath, userSpecifiedSchema)
val fileCatalog = new MetadataLogFileIndex(sparkSession, basePath,
caseInsensitiveOptions, userSpecifiedSchema)
val dataSchema = userSpecifiedSchema.orElse {
format.inferSchema(
sparkSession,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ abstract class PartitioningAwareFileIndex(

protected def leafDirToChildrenFiles: Map[Path, Array[FileStatus]]

protected lazy val pathGlobFilter = parameters.get("pathGlobFilter").map(new GlobFilter(_))
Comment thread
HyukjinKwon marked this conversation as resolved.
Outdated

protected def matchGlobPattern(file: FileStatus): Boolean = {
pathGlobFilter.forall(_.accept(file.getPath))
}

override def listFiles(
partitionFilters: Seq[Expression], dataFilters: Seq[Expression]): Seq[PartitionDirectory] = {
def isNonEmptyFile(f: FileStatus): Boolean = {
Expand All @@ -69,7 +75,7 @@ abstract class PartitioningAwareFileIndex(
val files: Seq[FileStatus] = leafDirToChildrenFiles.get(path) match {
case Some(existingDir) =>
// Directory has children files in it, return them
existingDir.filter(isNonEmptyFile)
existingDir.filter(f => matchGlobPattern(f) && isNonEmptyFile(f))

case None =>
// Directory does not exist, or has no children files
Expand All @@ -89,7 +95,7 @@ abstract class PartitioningAwareFileIndex(
override def sizeInBytes: Long = allFiles().map(_.getLen).sum

def allFiles(): Seq[FileStatus] = {
if (partitionSpec().partitionColumns.isEmpty) {
val files = if (partitionSpec().partitionColumns.isEmpty) {
// For each of the root input paths, get the list of files inside them
rootPaths.flatMap { path =>
// Make the path qualified (consistent with listLeafFiles and bulkListLeafFiles).
Expand Down Expand Up @@ -118,6 +124,7 @@ abstract class PartitioningAwareFileIndex(
} else {
leafFiles.values.toSeq
}
files.filter(matchGlobPattern)
}

protected def inferPartitioning(): PartitionSpec = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,10 @@ import org.apache.spark.util.SerializableConfiguration
* {{{
* // Scala
* val df = spark.read.format("binaryFile")
* .option("pathGlobFilter", "*.png")
* .load("/path/to/fileDir")
*
* // Java
* Dataset<Row> df = spark.read().format("binaryFile")
* .option("pathGlobFilter", "*.png")
* .load("/path/to/fileDir");
* }}}
*/
Expand Down Expand Up @@ -98,44 +96,37 @@ class BinaryFileFormat extends FileFormat with DataSourceRegister {

val broadcastedHadoopConf =
sparkSession.sparkContext.broadcast(new SerializableConfiguration(hadoopConf))
val binaryFileSourceOptions = new BinaryFileSourceOptions(options)
val pathGlobPattern = binaryFileSourceOptions.pathGlobFilter
val filterFuncs = filters.map(filter => createFilterFunction(filter))
val maxLength = sparkSession.conf.get(SOURCES_BINARY_FILE_MAX_LENGTH)

file: PartitionedFile => {
val path = new Path(file.filePath)
// TODO: Improve performance here: each file will recompile the glob pattern here.
if (pathGlobPattern.forall(new GlobFilter(_).accept(path))) {
val fs = path.getFileSystem(broadcastedHadoopConf.value.value)
val status = fs.getFileStatus(path)
if (filterFuncs.forall(_.apply(status))) {
val writer = new UnsafeRowWriter(requiredSchema.length)
writer.resetRowWriter()
requiredSchema.fieldNames.zipWithIndex.foreach {
case (PATH, i) => writer.write(i, UTF8String.fromString(status.getPath.toString))
case (LENGTH, i) => writer.write(i, status.getLen)
case (MODIFICATION_TIME, i) =>
writer.write(i, DateTimeUtils.fromMillis(status.getModificationTime))
case (CONTENT, i) =>
if (status.getLen > maxLength) {
throw new SparkException(
s"The length of ${status.getPath} is ${status.getLen}, " +
s"which exceeds the max length allowed: ${maxLength}.")
}
val stream = fs.open(status.getPath)
try {
writer.write(i, ByteStreams.toByteArray(stream))
} finally {
Closeables.close(stream, true)
}
case (other, _) =>
throw new RuntimeException(s"Unsupported field name: ${other}")
}
Iterator.single(writer.getRow)
} else {
Iterator.empty
val fs = path.getFileSystem(broadcastedHadoopConf.value.value)
val status = fs.getFileStatus(path)
if (filterFuncs.forall(_.apply(status))) {
val writer = new UnsafeRowWriter(requiredSchema.length)
writer.resetRowWriter()
requiredSchema.fieldNames.zipWithIndex.foreach {
case (PATH, i) => writer.write(i, UTF8String.fromString(status.getPath.toString))
case (LENGTH, i) => writer.write(i, status.getLen)
case (MODIFICATION_TIME, i) =>
writer.write(i, DateTimeUtils.fromMillis(status.getModificationTime))
case (CONTENT, i) =>
if (status.getLen > maxLength) {
throw new SparkException(
s"The length of ${status.getPath} is ${status.getLen}, " +
s"which exceeds the max length allowed: ${maxLength}.")
}
val stream = fs.open(status.getPath)
try {
writer.write(i, ByteStreams.toByteArray(stream))
} finally {
Closeables.close(stream, true)
}
case (other, _) =>
throw new RuntimeException(s"Unsupported field name: ${other}")
}
Iterator.single(writer.getRow)
} else {
Iterator.empty
}
Expand Down Expand Up @@ -204,14 +195,3 @@ object BinaryFileFormat {
}
}

class BinaryFileSourceOptions(
@transient private val parameters: CaseInsensitiveMap[String]) extends Serializable {

def this(parameters: Map[String, String]) = this(CaseInsensitiveMap(parameters))

/**
* An optional glob pattern to only include files with paths matching the pattern.
* The syntax follows [[org.apache.hadoop.fs.GlobFilter]].
*/
val pathGlobFilter: Option[String] = parameters.get("pathGlobFilter")
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import org.apache.hadoop.fs.{FileStatus, Path}
import org.apache.spark.deploy.SparkHadoopUtil
import org.apache.spark.internal.Logging
import org.apache.spark.sql.{DataFrame, Dataset, SparkSession}
import org.apache.spark.sql.catalyst.util.CaseInsensitiveMap
import org.apache.spark.sql.execution.datasources.{DataSource, InMemoryFileIndex, LogicalRelation}
import org.apache.spark.sql.types.StructType

Expand Down Expand Up @@ -195,7 +196,8 @@ class FileStreamSource(
private def allFilesUsingMetadataLogFileIndex() = {
// Note if `sourceHasMetadata` holds, then `qualifiedBasePath` is guaranteed to be a
// non-glob path
new MetadataLogFileIndex(sparkSession, qualifiedBasePath, None).allFiles()
new MetadataLogFileIndex(sparkSession, qualifiedBasePath,
CaseInsensitiveMap(options), None).allFiles()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ import org.apache.spark.sql.types.StructType
class MetadataLogFileIndex(
sparkSession: SparkSession,
path: Path,
parameters: Map[String, String],
userSpecifiedSchema: Option[StructType])
extends PartitioningAwareFileIndex(sparkSession, Map.empty, userSpecifiedSchema) {
extends PartitioningAwareFileIndex(sparkSession, parameters, userSpecifiedSchema) {

private val metadataDirectory = {
val metadataDir = new Path(path, FileStreamSink.metadataDir)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ final class DataStreamReader private[sql](sparkSession: SparkSession) extends Lo
* <ul>
* <li>`timeZone` (default session local timezone): sets the string that indicates a timezone
* to be used to parse timestamps in the JSON/CSV datasources or partition values.</li>
* <li>`pathGlobFilter`: an optional glob pattern to only include files with paths matching
* the pattern. The syntax follows <code>org.apache.hadoop.fs.GlobFilter</code>.
* It does not change the behavior of partition discovery.</li>
* </ul>
*
* @since 2.0.0
Expand Down Expand Up @@ -120,6 +123,9 @@ final class DataStreamReader private[sql](sparkSession: SparkSession) extends Lo
* <ul>
* <li>`timeZone` (default session local timezone): sets the string that indicates a timezone
* to be used to parse timestamps in the JSON/CSV data sources or partition values.</li>
* <li>`pathGlobFilter`: an optional glob pattern to only include files with paths matching
* the pattern. The syntax follows <code>org.apache.hadoop.fs.GlobFilter</code>.
* It does not change the behavior of partition discovery.</li>
* </ul>
*
* @since 2.0.0
Expand All @@ -136,6 +142,9 @@ final class DataStreamReader private[sql](sparkSession: SparkSession) extends Lo
* <ul>
* <li>`timeZone` (default session local timezone): sets the string that indicates a timezone
* to be used to parse timestamps in the JSON/CSV data sources or partition values.</li>
* <li>`pathGlobFilter`: an optional glob pattern to only include files with paths matching
* the pattern. The syntax follows <code>org.apache.hadoop.fs.GlobFilter</code>.
* It does not change the behavior of partition discovery.</li>
Comment thread
HyukjinKwon marked this conversation as resolved.
* </ul>
*
* @since 2.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,38 @@ class FileBasedDataSourceSuite extends QueryTest with SharedSQLContext with Befo
}
}

test("Option pathGlobFilter: filter files correctly") {
withTempPath { path =>
val dataDir = path.getCanonicalPath
Seq("foo").toDS().write.text(dataDir)
Seq("bar").toDS().write.mode("append").orc(dataDir)
val df = spark.read.option("pathGlobFilter", "*.txt").text(dataDir)
checkAnswer(df, Row("foo"))

// Both glob pattern in option and path should be effective to filter files.
val df2 = spark.read.option("pathGlobFilter", "*.txt").text(dataDir + "/*.orc")
checkAnswer(df2, Seq.empty)

val df3 = spark.read.option("pathGlobFilter", "*.txt").text(dataDir + "/*xt")
checkAnswer(df3, Row("foo"))
}
}

test("Option pathGlobFilter: simple extension filtering should contains partition info") {
withTempPath { path =>
val input = Seq(("foo", 1), ("oof", 2)).toDF("a", "b")
input.write.partitionBy("b").text(path.getCanonicalPath)
Seq("bar").toDS().write.mode("append").orc(path.getCanonicalPath + "/b=1")

// If we use glob pattern in the path, the partition column won't be shown in the result.
val df = spark.read.text(path.getCanonicalPath + "/*/*.txt")
checkAnswer(df, input.select("a"))

val df2 = spark.read.option("pathGlobFilter", "*.txt").text(path.getCanonicalPath)
checkAnswer(df2, input)
}
}

test("Return correct results when data columns overlap with partition columns") {
Seq("parquet", "orc", "json").foreach { format =>
withTempPath { path =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,25 @@ class FileStreamSourceSuite extends FileStreamSourceTest {
}
}

test("Option pathGlobFilter") {
val testTableName = "FileStreamSourceTest"
withTable(testTableName) {
withTempPath { output =>
Seq("foo").toDS().write.text(output.getCanonicalPath)
Seq("bar").toDS().write.mode("append").orc(output.getCanonicalPath)
val df = spark.readStream.option("pathGlobFilter", "*.txt")
.format("text").load(output.getCanonicalPath)
val query = df.writeStream.format("memory").queryName(testTableName).start()
try {
query.processAllAvailable()
checkDatasetUnorderly(spark.table(testTableName).as[String], "foo")
} finally {
query.stop()
}
}
}
}

test("read from textfile") {
withTempDirs { case (src, tmp) =>
val textStream = spark.readStream.textFile(src.getCanonicalPath)
Expand Down