Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
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
19 changes: 19 additions & 0 deletions python/pyspark/sql/readwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,25 @@ def mode(self, saveMode):
self._jwrite = self._jwrite.mode(saveMode)
return self

@since(2.0)
def outputMode(self, outputMode):
"""Specifies how data of a streaming DataFrame/Dataset is written to a streaming sink.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: add .. note:: Experimental.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done.

Options include:

* `append`:Only the new rows in the streaming DataFrame/Dataset will be written to
the sink
* `update`:Only the changed rows in the streaming DataFrame/Dataset will be written to

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: remove this line

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good catch. fixed.

the sink every time there is some updates
* `complete`:All the rows in the streaming DataFrame/Dataset will be written to the sink
every time these is some updates

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.

each time the trigger fires?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I want to write something that makes sense generally, without understanding trigger and all. As is, since the trigger is optional, one does not need to know about triggers at all to start running stuff in structured streaming.


>>> sdf.write.outputMode('append')
"""
if outputMode is not None:
self._jwrite = self._jwrite.outputMode(outputMode)
return self

@since(1.4)
def format(self, source):
"""Specifies the underlying output data source.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
* limitations under the License.
*/

package org.apache.spark.sql.catalyst.analysis
package org.apache.spark.sql;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: move this file to sql/catalyst/src/main/java/org/apache/spark/sql/OutputMode.java

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

before that.... i realize that making this java enum prevents us from having output modes like UpdateInPlace("key") in the future. So we have to think about this.


sealed trait OutputMode

case object Append extends OutputMode
case object Update extends OutputMode
public enum OutputMode {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: @Experimental

Append,
Update,

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.

This actually raises a good question. I'm not sure if we can use enums here as I think that we need to have a notion of a key in order to do an Update mode.

Complete
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.spark.sql.catalyst.analysis

import org.apache.spark.sql.AnalysisException
import org.apache.spark.sql.OutputMode
import org.apache.spark.sql.catalyst.plans._
import org.apache.spark.sql.catalyst.plans.logical._

Expand All @@ -36,7 +37,7 @@ object UnsupportedOperationChecker {
}
}

def checkForStreaming(plan: LogicalPlan, outputMode: OutputMode): Unit = {
def checkForStreaming(implicit plan: LogicalPlan, outputMode: OutputMode): Unit = {

if (!plan.isStreaming) {
throwError(
Expand All @@ -55,21 +56,6 @@ object UnsupportedOperationChecker {
case _: InsertIntoTable =>
throwError("InsertIntoTable is not supported with streaming DataFrames/Datasets")

case Aggregate(_, _, child) if child.isStreaming =>
if (outputMode == Append) {
throwError(
"Aggregations are not supported on streaming DataFrames/Datasets in " +
"Append output mode. Consider changing output mode to Update.")
}
val moreStreamingAggregates = child.find {
case Aggregate(_, _, grandchild) if grandchild.isStreaming => true
case _ => false
}
if (moreStreamingAggregates.nonEmpty) {
throwError("Multiple streaming aggregations are not supported with " +
"streaming DataFrames/Datasets")
}

case Join(left, right, joinType, _) =>

joinType match {
Expand Down Expand Up @@ -138,6 +124,26 @@ object UnsupportedOperationChecker {
case _ =>
}
}

// Checks related to aggregations
val aggregates = plan.collect { case a @ Aggregate(_, _, _) if a.isStreaming => a }
outputMode match {
case OutputMode.Append if aggregates.nonEmpty =>
throwError(
s"$outputMode output mode not supported with streaming aggregates on " +
s"streaming DataFrames/DataSets")

case OutputMode.Complete | OutputMode.Update if aggregates.isEmpty =>
throwError(
s"$outputMode output mode not supported when not streaming aggregates are present on " +
s"streaming DataFrames/Datasets")

case _ =>
}
if (aggregates.size > 1) {
throwError(
"Multiple streaming aggregations are not supported with streaming DataFrames/Datasets")
}
}

private def throwErrorIf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
package org.apache.spark.sql.catalyst.analysis

import org.apache.spark.SparkFunSuite
import org.apache.spark.sql.AnalysisException
import org.apache.spark.sql.{AnalysisException, OutputMode}
import org.apache.spark.sql.OutputMode._
import org.apache.spark.sql.catalyst.FunctionIdentifier
import org.apache.spark.sql.catalyst.dsl.expressions._
import org.apache.spark.sql.catalyst.dsl.plans._
Expand Down Expand Up @@ -204,7 +205,6 @@ class UnsupportedOperationsSuite extends SparkFunSuite {
_.intersect(_),
streamStreamSupported = false)


// Unary operations
testUnaryOperatorInStreamingPlan("sort", Sort(Nil, true, _))
testUnaryOperatorInStreamingPlan("sort partitions", SortPartitions(Nil, _), expectedMsg = "sort")
Expand All @@ -213,6 +213,10 @@ class UnsupportedOperationsSuite extends SparkFunSuite {
testUnaryOperatorInStreamingPlan(
"window", Window(Nil, Nil, Nil, _), expectedMsg = "non-time-based windows")

// Output modes with aggregation and non-aggregation plans
testOutputMode(OutputMode.Append, shouldSupportAggregation = false)
testOutputMode(OutputMode.Update, shouldSupportAggregation = true)
testOutputMode(OutputMode.Complete, shouldSupportAggregation = true)

/*
=======================================================================================
Expand Down Expand Up @@ -311,6 +315,37 @@ class UnsupportedOperationsSuite extends SparkFunSuite {
outputMode)
}

def testOutputMode(
outputMode: OutputMode,
shouldSupportAggregation: Boolean): Unit = {

// aggregation
if (shouldSupportAggregation) {
assertNotSupportedInStreamingPlan(
s"$outputMode output mode - no aggregation",
streamRelation.where($"a" > 1),
outputMode = outputMode,
Seq("aggregation", s"$outputMode output mode"))

assertSupportedInStreamingPlan(
s"$outputMode output mode - aggregation",
streamRelation.groupBy("a")("count(*)"),
outputMode = outputMode)

} else {
assertSupportedInStreamingPlan(
s"$outputMode output mode - no aggregation",
streamRelation.where($"a" > 1),
outputMode = outputMode)

assertNotSupportedInStreamingPlan(
s"$outputMode output mode - aggregation",
streamRelation.groupBy("a")("count(*)"),
outputMode = outputMode,
Seq("aggregation", s"$outputMode output mode"))
}
}

/**
* Assert that the logical plan is supported as subplan insider a streaming plan.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ package org.apache.spark.sql
import scala.collection.mutable

import org.apache.spark.annotation.Experimental
import org.apache.spark.sql.catalyst.analysis.{Append, OutputMode, UnsupportedOperationChecker}
import org.apache.spark.sql.catalyst.analysis.UnsupportedOperationChecker
import org.apache.spark.sql.execution.streaming._
import org.apache.spark.sql.execution.streaming.state.StateStoreCoordinatorRef
import org.apache.spark.sql.internal.SQLConf
Expand Down Expand Up @@ -175,9 +175,9 @@ class ContinuousQueryManager(sparkSession: SparkSession) {
checkpointLocation: String,
df: DataFrame,
sink: Sink,
outputMode: OutputMode,
trigger: Trigger = ProcessingTime(0),
triggerClock: Clock = new SystemClock(),
outputMode: OutputMode = Append): ContinuousQuery = {
triggerClock: Clock = new SystemClock()): ContinuousQuery = {
activeQueriesLock.synchronized {
if (activeQueries.contains(name)) {
throw new IllegalArgumentException(
Expand Down
57 changes: 54 additions & 3 deletions sql/core/src/main/scala/org/apache/spark/sql/DataFrameWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,50 @@ final class DataFrameWriter private[sql](df: DataFrame) {
case "ignore" => SaveMode.Ignore
case "error" | "default" => SaveMode.ErrorIfExists
case _ => throw new IllegalArgumentException(s"Unknown save mode: $saveMode. " +
"Accepted modes are 'overwrite', 'append', 'ignore', 'error'.")
"Accepted save modes are 'overwrite', 'append', 'ignore', 'error'.")

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.

We might consider aliasing mode as saveMode and deprecating mode.

/cc @rxin

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was thinking the same.

}
this
}

/**
* Specifies how data of a streaming DataFrame/Dataset is written to a streaming sink.
* - `OutputMode.Append`: only the new rows in the streaming DataFrame/Dataset will be
* written to the sink
* - `OutputMode.Update`: only the changed rows in the streaming DataFrame/Dataset will be
* written to the sink every time there is some updates
* - `OutputMode.Complete`: all the rows in the streaming DataFrame/Dataset will be written
* to the sink every time these is some updates
*
* @since 2.0.0
*/
@Experimental
def outputMode(outputMode: OutputMode): DataFrameWriter = {
assertStreaming("outputMode() can only be called on continuous queries")
this.outputMode = outputMode
this
}


/**
* Specifies how data of a streaming DataFrame/Dataset is written to a streaming sink.
* - `append`: only the new rows in the streaming DataFrame/Dataset will be written to
* the sink
* - `update`: only the changed rows in the streaming DataFrame/Dataset will be written to
* the sink every time there is some updates
* - `complete`: all the rows in the streaming DataFrame/Dataset will be written to the sink
* every time these is some updates
*
* @since 2.0.0
*/
@Experimental
def outputMode(outputMode: String): DataFrameWriter = {

@zsxwing zsxwing May 27, 2016

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@tdas do we need to think about how to support the update mode for this method? outputMode("update(columnName)")?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That we can decide later. That sounds too complicated to reason about right now when we have not even finalized how to specify Update mode.

assertStreaming("outputMode() can only be called on continuous queries")
this.outputMode = outputMode.toLowerCase match {
case "append" => OutputMode.Append
case "update" => OutputMode.Update
case "complete" => OutputMode.Complete
case _ => throw new IllegalArgumentException(s"Unknown output mode $outputMode. " +
"Accepted output modes are 'append', 'update', 'complete'")
}
this
}
Expand Down Expand Up @@ -319,14 +362,19 @@ final class DataFrameWriter private[sql](df: DataFrame) {
checkpointPath.toUri.toString
}

val sink = new MemorySink(df.schema)
if (!Seq(OutputMode.Append, OutputMode.Complete).contains(outputMode)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: maybe move this logic to the constructor of MemorySink so that we can make sure no place will pass a wrong OutputMode to MemorySink.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The tricky thing is that we want to make the Memory Sink compatible with update internally, but we may not want public API to support update mode yet.

throw new IllegalArgumentException(s"Memory sink does not support output mode $outputMode")
}

val sink = new MemorySink(df.schema, outputMode)
val resultDf = Dataset.ofRows(df.sparkSession, new MemoryPlan(sink))
resultDf.createOrReplaceTempView(queryName)
val continuousQuery = df.sparkSession.sessionState.continuousQueryManager.startQuery(
queryName,
checkpointLocation,
df,
sink,
outputMode,
trigger)
continuousQuery
} else {
Expand All @@ -352,7 +400,8 @@ final class DataFrameWriter private[sql](df: DataFrame) {
queryName,
checkpointLocation,
df,
dataSource.createSink(),
dataSource.createSink(outputMode),
outputMode,
trigger)
}
}
Expand Down Expand Up @@ -705,6 +754,8 @@ final class DataFrameWriter private[sql](df: DataFrame) {

private var mode: SaveMode = SaveMode.ErrorIfExists

private var outputMode: OutputMode = OutputMode.Append

private var trigger: Trigger = ProcessingTime(0L)

private var extraOptions = new scala.collection.mutable.HashMap[String, String]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.apache.spark.sql.execution.aggregate

import org.apache.spark.sql.OutputMode
import org.apache.spark.sql.catalyst.expressions._
import org.apache.spark.sql.catalyst.expressions.aggregate._
import org.apache.spark.sql.execution.SparkPlan
Expand All @@ -33,7 +34,7 @@ object Utils {
resultExpressions: Seq[NamedExpression],
child: SparkPlan): Seq[SparkPlan] = {

val completeAggregateExpressions = aggregateExpressions.map(_.copy(mode = Complete))
val completeAggregateExpressions = aggregateExpressions.map(_.copy(mode = aggregate.Complete))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

nit: not needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed.

val completeAggregateAttributes = completeAggregateExpressions.map(_.resultAttribute)
SortBasedAggregateExec(
requiredChildDistributionExpressions = Some(groupingExpressions),
Expand Down Expand Up @@ -311,8 +312,8 @@ object Utils {
aggregateExpressions.flatMap(_.aggregateFunction.inputAggBufferAttributes),
child = restored)
}

val saved = StateStoreSaveExec(groupingAttributes, None, partialMerged2)
val saved = StateStoreSaveExec(
groupingAttributes, stateId = None, returnAllStates = None, partialMerged2)

val finalAndCompleteAggregate: SparkPlan = {
val finalAggregateExpressions = functionsWithoutDistinct.map(_.copy(mode = Final))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,15 +225,20 @@ case class DataSource(
}

/** Returns a sink that can be used to continually write data. */
def createSink(): Sink = {
def createSink(outputMode: OutputMode): Sink = {
providingClass.newInstance() match {
case s: StreamSinkProvider => s.createSink(sparkSession.sqlContext, options, partitionColumns)
case s: StreamSinkProvider =>
s.createSink(sparkSession.sqlContext, options, partitionColumns, outputMode)

case parquet: parquet.DefaultSource =>
val caseInsensitiveOptions = new CaseInsensitiveMap(options)
val path = caseInsensitiveOptions.getOrElse("path", {
throw new IllegalArgumentException("'path' is not specified")
})
if (outputMode != OutputMode.Append) {
throw new IllegalArgumentException(
s"Data source $className does not support $outputMode output mode")
}
new FileStreamSink(sparkSession, path, parquet, partitionColumns, options)

case _ =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@

package org.apache.spark.sql.execution.streaming

import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.catalyst.analysis.OutputMode
import org.apache.spark.sql.{OutputMode, SparkSession}
import org.apache.spark.sql.catalyst.plans.logical._
import org.apache.spark.sql.catalyst.rules.Rule
import org.apache.spark.sql.execution.{QueryExecution, SparkPlan, SparkPlanner, UnaryExecNode}
Expand Down Expand Up @@ -53,16 +52,19 @@ class IncrementalExecution private[sql](

/** Locates save/restore pairs surrounding aggregation. */
val state = new Rule[SparkPlan] {

override def apply(plan: SparkPlan): SparkPlan = plan transform {
case StateStoreSaveExec(keys, None,
case StateStoreSaveExec(keys, None, None,
UnaryExecNode(agg,
StateStoreRestoreExec(keys2, None, child))) =>
val stateId = OperatorStateId(checkpointLocation, operatorId, currentBatchId)
val returnAllStates = if (outputMode == OutputMode.Complete) true else false
operatorId += 1

StateStoreSaveExec(
keys,
Some(stateId),
Some(returnAllStates),
agg.withNewChildren(
StateStoreRestoreExec(
keys,
Expand Down
Loading