johanl-db commented on code in PR #56617:
URL: https://github.com/apache/spark/pull/56617#discussion_r3543694651


##########
sql/core/src/main/scala/org/apache/spark/sql/classic/DataFrameWriter.scala:
##########
@@ -168,66 +168,90 @@ final class DataFrameWriter[T] private[sql](ds: 
Dataset[T]) extends sql.DataFram
 
       import 
org.apache.spark.sql.execution.datasources.v2.DataSourceV2Implicits._
       val catalogManager = df.sparkSession.sessionState.catalogManager
+
+      def createTableAsSelectCommand(
+          catalog: TableCatalog, ident: Identifier, ignoreIfExists: Boolean): 
LogicalPlan = {
+        val tableSpec = UnresolvedTableSpec(
+          properties = Map.empty,
+          provider = Some(source),
+          optionExpression = OptionList(Seq.empty),
+          location = extraOptions.get("path"),
+          comment = extraOptions.get(TableCatalog.PROP_COMMENT),
+          collation = extraOptions.get(TableCatalog.PROP_COLLATION),
+          serde = None,
+          external = false,
+          constraints = Seq.empty)
+        CreateTableAsSelect(
+          UnresolvedIdentifier(
+            catalog.name +: ident.namespace.toImmutableArraySeq :+ ident.name),
+          partitioningAsV2,
+          df.queryExecution.analyzed,
+          tableSpec,
+          finalOptions,
+          ignoreIfExists = ignoreIfExists)
+      }
+
+      def appendOrOverwriteCommand(

Review Comment:
   Right, this should be explicitly blocked, I added a guard + test



##########
sql/core/src/test/scala/org/apache/spark/sql/connector/SupportsCatalogOptionsSuite.scala:
##########
@@ -443,3 +494,13 @@ class CatalogSupportingInMemoryTableProvider
     }
   }
 }
+
+/** Opts out of catalog resolution, so load/save fall back to the plain 
TableProvider path. */
+class CatalogResolutionOptOutProvider extends 
CatalogSupportingInMemoryTableProvider {
+  override def useCatalogResolution(options: CaseInsensitiveStringMap): 
Boolean = false
+}
+
+/** Opts out of failing on a missing table, enabling create-on-write for 
save(). */
+class CreateOnWriteProvider extends CatalogSupportingInMemoryTableProvider {
+  override def failWriteIfTableDoesNotExist(options: 
CaseInsensitiveStringMap): Boolean = false
+}

Review Comment:
   Done



##########
sql/core/src/main/scala/org/apache/spark/sql/classic/DataFrameWriter.scala:
##########
@@ -168,66 +168,90 @@ final class DataFrameWriter[T] private[sql](ds: 
Dataset[T]) extends sql.DataFram
 
       import 
org.apache.spark.sql.execution.datasources.v2.DataSourceV2Implicits._
       val catalogManager = df.sparkSession.sessionState.catalogManager
+
+      def createTableAsSelectCommand(
+          catalog: TableCatalog, ident: Identifier, ignoreIfExists: Boolean): 
LogicalPlan = {
+        val tableSpec = UnresolvedTableSpec(
+          properties = Map.empty,
+          provider = Some(source),
+          optionExpression = OptionList(Seq.empty),
+          location = extraOptions.get("path"),
+          comment = extraOptions.get(TableCatalog.PROP_COMMENT),
+          collation = extraOptions.get(TableCatalog.PROP_COLLATION),
+          serde = None,
+          external = false,
+          constraints = Seq.empty)
+        CreateTableAsSelect(
+          UnresolvedIdentifier(
+            catalog.name +: ident.namespace.toImmutableArraySeq :+ ident.name),
+          partitioningAsV2,
+          df.queryExecution.analyzed,
+          tableSpec,
+          finalOptions,
+          ignoreIfExists = ignoreIfExists)
+      }
+
+      def appendOrOverwriteCommand(
+          table: Table,
+          catalog: Option[CatalogPlugin],
+          ident: Option[Identifier]): LogicalPlan = {
+        checkPartitioningMatchesV2Table(table)
+        val relation = DataSourceV2Relation.create(table, catalog, ident, 
dsOptions)
+        if (curmode == SaveMode.Append) {
+          AppendData.byName(relation, df.logicalPlan, finalOptions, 
_withSchemaEvolution)
+        } else {
+          // Truncate the table. TableCapabilityCheck will throw a nice 
exception if this
+          // isn't supported
+          OverwriteByExpression.byName(
+            relation, df.logicalPlan, Literal(true), finalOptions, 
_withSchemaEvolution)
+        }
+      }
+
       curmode match {
         case SaveMode.Append | SaveMode.Overwrite =>
-          val (table, catalog, ident) = provider match {
-            case supportsExtract: SupportsCatalogOptions =>
+          provider match {
+            case supportsExtract: SupportsCatalogOptions
+                if supportsExtract.useCatalogResolution(dsOptions) =>
               val ident = supportsExtract.extractIdentifier(dsOptions)
               val catalog = CatalogV2Util.getTableProviderCatalog(
                 supportsExtract, catalogManager, dsOptions)
-
-              (catalog.loadTable(ident), Some(catalog), Some(ident))
+              val tableOpt =
+                try Some(catalog.loadTable(ident))

Review Comment:
   I *think* we should pass write privileges here but that's unrelated to this 
change so I'd rather keep it out, esp. since I don't fully understand the 
consequences of passing privileges.



##########
sql/core/src/test/scala/org/apache/spark/sql/connector/SupportsCatalogOptionsSuite.scala:
##########
@@ -370,6 +372,55 @@ class SupportsCatalogOptionsSuite extends 
SharedSparkSession with BeforeAndAfter
       .contains("Cannot specify both version and timestamp when time 
travelling the table."))
   }
 
+  test("useCatalogResolution=false: read is resolved via the TableProvider 
path, not the catalog") {
+    // The provider opts out of catalog resolution, so load() goes through 
getTable instead of
+    // extractIdentifier/extractCatalog. The resulting relation therefore has 
no catalog/identifier.
+    val df = spark.read.format(optOutFormat).option("name", "t1").load()
+    val relation = df.logicalPlan.collectFirst {
+      case r: DataSourceV2Relation => r
+    }.getOrElse(fail("Expected a DataSourceV2Relation"))
+    assert(relation.catalog.isEmpty && relation.identifier.isEmpty,
+      "Opting out of catalog resolution should bypass the catalog")
+  }
+
+  test("useCatalogResolution=false: a user-specified schema is allowed (no 
catalog check)") {
+    // The schema check only fires on the catalog-resolution path; opting out 
skips it.
+    val df = spark.read.format(optOutFormat).option("name", "t1").schema("i 
int, j int").load()
+    assert(df.schema.fieldNames === Array("i", "j"))
+  }
+
+  test("failWriteIfTableDoesNotExist=false: append creates a missing table 
(create-on-write)") {
+    val df = spark.range(10)
+    // t1 does not exist yet; append should create it from the query instead 
of failing.
+    df.write.format(createOnWriteFormat).option("name", 
"t1").option("catalog", catalogName)
+      .mode(SaveMode.Append).save()
+    assert(catalog(catalogName).tableExists("t1"), "append should have created 
the table")
+    checkAnswer(load("t1", Some(catalogName)), df.toDF())
+  }
+
+  test("failWriteIfTableDoesNotExist=false: overwrite creates a missing table 
(create-on-write)") {
+    val df = spark.range(10, 20)
+    df.write.format(createOnWriteFormat).option("name", 
"t1").option("catalog", catalogName)
+      .mode(SaveMode.Overwrite).save()
+    assert(catalog(catalogName).tableExists("t1"), "overwrite should have 
created the table")
+    checkAnswer(load("t1", Some(catalogName)), df.toDF())
+  }
+
+  test("failWriteIfTableDoesNotExist=false: append to an existing table still 
appends") {
+    sql(s"create table $catalogName.t1 (id bigint) using $createOnWriteFormat")
+    spark.range(10).write.format(createOnWriteFormat).option("name", "t1")
+      .option("catalog", catalogName).mode(SaveMode.Append).save()
+    checkAnswer(load("t1", Some(catalogName)), spark.range(10).toDF())
+  }
+
+  test("append to a missing table fails by default 
(failWriteIfTableDoesNotExist=true)") {
+    // The default provider keeps the prior behavior: append/overwrite to a 
missing table fails.
+    intercept[NoSuchTableException] {
+      spark.range(10).write.format(format).option("name", 
"t1").option("catalog", catalogName)
+        .mode(SaveMode.Append).save()
+    }
+  }
+

Review Comment:
   Added tests



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