-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-40737][CONNECT] Add basic support for DataFrameWriter #38192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
c4ae79e
6d152e2
cdf41d6
139873e
36d320b
7a3a16c
12a06fe
f681c72
5af6799
dc200c8
1d70250
85f81c0
0dc03ac
a13f9e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,10 +24,16 @@ import com.google.common.collect.{Lists, Maps} | |
| import org.apache.spark.annotation.{Since, Unstable} | ||
| import org.apache.spark.api.python.{PythonEvalType, SimplePythonFunction} | ||
| import org.apache.spark.connect.proto | ||
| import org.apache.spark.sql.SparkSession | ||
| import org.apache.spark.connect.proto.WriteOperation | ||
| import org.apache.spark.sql.{Dataset, SparkSession} | ||
| import org.apache.spark.sql.connect.planner.SparkConnectPlanner | ||
| import org.apache.spark.sql.execution.python.UserDefinedPythonFunction | ||
| import org.apache.spark.sql.types.StringType | ||
|
|
||
| final case class InvalidCommandInput( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to have a custom exception for when we rethrow.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is a user-facing error, we should actually leverage errorframe work we have .. cc @gengliangwang @MaxGekk @itholic
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm happy to fix this as a follow up, does it make sense? The errors are reported back through grpc. If you point me to the right base class I can fix it then. |
||
| private val message: String = "", | ||
| private val cause: Throwable = None.orNull) | ||
|
grundprinzip marked this conversation as resolved.
Outdated
|
||
| extends Exception(message, cause) | ||
|
|
||
| @Unstable | ||
| @Since("3.4.0") | ||
|
|
@@ -40,17 +46,19 @@ class SparkConnectCommandPlanner(session: SparkSession, command: proto.Command) | |
| command.getCommandTypeCase match { | ||
| case proto.Command.CommandTypeCase.CREATE_FUNCTION => | ||
| handleCreateScalarFunction(command.getCreateFunction) | ||
| case proto.Command.CommandTypeCase.WRITE_OPERATION => | ||
| handleWriteOperation(command.getWriteOperation) | ||
| case _ => throw new UnsupportedOperationException(s"$command not supported.") | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * This is a helper function that registers a new Python function in the SparkSession. | ||
| * | ||
| * Right now this function is very rudimentary and bare-bones just to showcase how it | ||
| * is possible to remotely serialize a Python function and execute it on the Spark cluster. | ||
| * If the Python version on the client and server diverge, the execution of the function that | ||
| * is serialized will most likely fail. | ||
| * Right now this function is very rudimentary and bare-bones just to showcase how it is | ||
| * possible to remotely serialize a Python function and execute it on the Spark cluster. If the | ||
| * Python version on the client and server diverge, the execution of the function that is | ||
| * serialized will most likely fail. | ||
|
grundprinzip marked this conversation as resolved.
Outdated
|
||
| * | ||
| * @param cf | ||
| */ | ||
|
|
@@ -74,4 +82,60 @@ class SparkConnectCommandPlanner(session: SparkSession, command: proto.Command) | |
| session.udf.registerPython(cf.getPartsList.asScala.head, udf) | ||
| } | ||
|
|
||
| /** | ||
| * Transforms the write operation and executes it. | ||
| * | ||
| * The input write operation contains a reference to the input plan and transforms it to the | ||
| * corresponding logical plan. Afterwards, creates the DataFrameWriter and translates the | ||
| * parameters of the WriteOperation into the corresponding methods calls. | ||
| * | ||
| * @param writeOperation | ||
| */ | ||
| def handleWriteOperation(writeOperation: WriteOperation): Unit = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a bit weird to have this in the SparkPlanner node, but I guess this is the consequence of the builder() API we have in the DataFrameWriter. @cloud-fan AFAIK you have been working on making writes more declarative (i.e. planned writes). Do you see a way to improve this?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @allisonwang-db FYI
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is more than planned write. We need to create a logical plan for DF write, instead of putting implementation code in DF write APIs. |
||
| // Transform the input plan into the logical plan. | ||
| val planner = new SparkConnectPlanner(writeOperation.getInput, session) | ||
| val plan = planner.transform() | ||
| // And create a Dataset from the plan. | ||
| val dataset = Dataset.ofRows(session, logicalPlan = plan) | ||
|
|
||
| val w = dataset.write | ||
| if (writeOperation.getOptionsCount > 0) { | ||
| writeOperation.getOptionsList.asScala.foreach(x => w.option(x.getKey, x.getValue)) | ||
| } | ||
|
|
||
| if (writeOperation.getSortColumnNamesCount > 0) { | ||
| val names = writeOperation.getSortColumnNamesList.asScala | ||
| w.sortBy(names.head, names.tail.toSeq: _*) | ||
| } | ||
|
|
||
| if (writeOperation.hasBucketBy) { | ||
| val op = writeOperation.getBucketBy | ||
| val cols = op.getColumnsList.asScala | ||
| if (op.getBucketCount <= 0) { | ||
| throw InvalidCommandInput( | ||
| s"BucketBy must specify a bucket count > 0, received ${op.getBucketCount} instead.") | ||
| } | ||
| w.bucketBy(op.getBucketCount, cols.head, cols.tail.toSeq: _*) | ||
| } | ||
|
|
||
| if (writeOperation.getPartitionByColumnsCount > 0) { | ||
| val names = writeOperation.getPartitionByColumnsList.asScala | ||
| w.partitionBy(names.toSeq: _*) | ||
| } | ||
|
|
||
| if (writeOperation.getFormat != null) { | ||
|
grundprinzip marked this conversation as resolved.
Outdated
|
||
| w.format(writeOperation.getFormat) | ||
| } | ||
|
|
||
| writeOperation.getSaveTypeCase match { | ||
| case proto.WriteOperation.SaveTypeCase.PATH => w.save(writeOperation.getPath) | ||
| case proto.WriteOperation.SaveTypeCase.TABLE_NAME => | ||
| w.saveAsTable(writeOperation.getTableName) | ||
| case _ => | ||
| throw new UnsupportedOperationException( | ||
| s"WriteOperation:SaveTypeCase not supported " | ||
| + "${writeOperation.getSaveTypeCase.getNumber}") | ||
|
grundprinzip marked this conversation as resolved.
Outdated
grundprinzip marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.