Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ import org.apache.hudi.common.util.PartitionPathEncodeUtils
import org.apache.hudi.{AvroConversionUtils, SparkAdapterSupport}
import org.apache.spark.api.java.JavaSparkContext
import org.apache.spark.sql.catalyst.TableIdentifier
import org.apache.spark.sql.catalyst.analysis.{Resolver, UnresolvedRelation}
import org.apache.spark.sql.catalyst.catalog.{CatalogTable, CatalogTableType, HoodieCatalogTable}
import org.apache.spark.sql.catalyst.analysis.Resolver
import org.apache.spark.sql.catalyst.catalog.CatalogTypes.TablePartitionSpec
import org.apache.spark.sql.catalyst.catalog.{CatalogTable, HoodieCatalogTable}
import org.apache.spark.sql.catalyst.expressions.{And, Attribute, Cast, Expression, Literal}
import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, SubqueryAlias}
import org.apache.spark.sql.execution.datasources.LogicalRelation
import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
import org.apache.spark.sql.types.{DataType, NullType, StringType, StructField, StructType}
import org.apache.spark.sql.types._
import org.apache.spark.sql.{AnalysisException, Column, DataFrame, SparkSession}

import java.net.URI
Expand Down Expand Up @@ -128,6 +128,7 @@ object HoodieSqlCommonUtils extends SparkAdapterSupport {

/**
* Add the hoodie meta fields to the schema.
*
* @param schema
* @return
*/
Expand All @@ -143,6 +144,7 @@ object HoodieSqlCommonUtils extends SparkAdapterSupport {

/**
* Remove the meta fields from the schema.
*
* @param schema
* @return
*/
Expand Down Expand Up @@ -171,6 +173,7 @@ object HoodieSqlCommonUtils extends SparkAdapterSupport {

/**
* Get the table location.
*
* @param tableId
* @param spark
* @return
Expand Down Expand Up @@ -233,6 +236,7 @@ object HoodieSqlCommonUtils extends SparkAdapterSupport {

/**
* Split the expression to a sub expression seq by the AND operation.
*
* @param expression
* @return
*/
Expand Down Expand Up @@ -262,15 +266,15 @@ object HoodieSqlCommonUtils extends SparkAdapterSupport {
* Currently we support three kinds of instant time format for time travel query:
* 1、yyyy-MM-dd HH:mm:ss
* 2、yyyy-MM-dd
* This will convert to 'yyyyMMdd000000'.
* This will convert to 'yyyyMMdd000000'.
* 3、yyyyMMddHHmmss
*/
def formatQueryInstant(queryInstant: String): String = {
val instantLength = queryInstant.length
if (instantLength == 19 || instantLength == 23) { // for yyyy-MM-dd HH:mm:ss[.SSS]
HoodieInstantTimeGenerator.getInstantForDateString(queryInstant)
} else if (instantLength == HoodieInstantTimeGenerator.SECS_INSTANT_ID_LENGTH
|| instantLength == HoodieInstantTimeGenerator.MILLIS_INSTANT_ID_LENGTH) { // for yyyyMMddHHmmss[SSS]
|| instantLength == HoodieInstantTimeGenerator.MILLIS_INSTANT_ID_LENGTH) { // for yyyyMMddHHmmss[SSS]
HoodieActiveTimeline.parseDateFromInstantTime(queryInstant) // validate the format
queryInstant
} else if (instantLength == 10) { // for yyyy-MM-dd
Expand Down Expand Up @@ -300,7 +304,7 @@ object HoodieSqlCommonUtils extends SparkAdapterSupport {

// Find the origin column from schema by column name, throw an AnalysisException if the column
// reference is invalid.
def findColumnByName(schema: StructType, name: String, resolver: Resolver):Option[StructField] = {
def findColumnByName(schema: StructType, name: String, resolver: Resolver): Option[StructField] = {
schema.fields.collectFirst {
case field if resolver(field.name, name) => field
}
Expand Down Expand Up @@ -372,4 +376,32 @@ object HoodieSqlCommonUtils extends SparkAdapterSupport {
}.mkString(",")
partitionsToDrop
}

def getPartitionPathToTruncate(hoodieCatalogTable: HoodieCatalogTable,
Comment thread
xushiyan marked this conversation as resolved.
Outdated
table: CatalogTable,
partitionSpec: Option[TablePartitionSpec],
resolver: Resolver): String = {
val partCols = table.partitionColumnNames
val normalizedSpec: Seq[Map[String, String]] = Seq(partitionSpec.map { spec =>
normalizePartitionSpec(
spec,
partCols,
table.identifier.quotedString,
resolver)
}.get)

val allPartitionPaths = hoodieCatalogTable.getPartitionPaths
val enableEncodeUrl = isUrlEncodeEnabled(allPartitionPaths, table)

val partitionsToTruncate = normalizedSpec.map { spec =>
hoodieCatalogTable.partitionFields.map { partitionColumn =>
if (enableEncodeUrl) {
partitionColumn + "=" + "\"" + spec(partitionColumn) + "\""

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.

this encode case did not call PartitionPathEncodeUtils.escapePathName?

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.

yeah. not sure if he is confusing w/ hive style partitioning. Don't we need to consider both? i.e. url encode and hive style partitioning ?

snippet from KeyGenUtils

    if (encodePartitionPath) {
      partitionPath = PartitionPathEncodeUtils.escapePathName(partitionPath);
    }
    if (hiveStylePartitioning) {
      partitionPath = partitionPathField + "=" + partitionPath;
    }

@XuQianJin-Stars XuQianJin-Stars Apr 13, 2022

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.

this encode case did not call PartitionPathEncodeUtils.escapePathName?

The urlcode character appears in the partition, which cannot be deleted with single quotation marks. Double quotation marks are used after url decoding.

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.

hive style partitioning

Contains the processing of hive style partitioning, which is mainly to construct the where condition of delete sql.

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.

I see w/ latest commit, all changes in HoodieSqlCommonutils is reverted. So, where exactly we process url decoding ?

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.

guess getPartitionPathToDrop() does that.

} else {
partitionColumn + "=" + "'" + spec(partitionColumn) + "'"
}
}.mkString(" and ")
}.mkString
partitionsToTruncate
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import org.apache.hudi.DataSourceWriteOptions._
import org.apache.hudi.common.config.TypedProperties
import org.apache.hudi.common.model.OverwriteWithLatestAvroPayload
import org.apache.hudi.common.table.HoodieTableConfig
import org.apache.hudi.config.HoodieWriteConfig.TBL_NAME
import org.apache.hudi.config.HoodieWriteConfig.{AVRO_SCHEMA_VALIDATE_ENABLE, TBL_NAME}
Comment thread
xushiyan marked this conversation as resolved.
Outdated
import org.apache.hudi.config.{HoodieIndexConfig, HoodieWriteConfig}
import org.apache.hudi.hive.ddl.HiveSyncMode
import org.apache.hudi.hive.{HiveSyncConfig, MultiPartKeysValueExtractor}
Expand All @@ -40,7 +40,6 @@ import org.apache.spark.sql.types.StructType

import java.util
import java.util.Locale

import scala.collection.JavaConverters._

trait ProvidesHoodieConfig extends Logging {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,12 @@

package org.apache.spark.sql.hudi.command

import org.apache.hadoop.fs.Path
import org.apache.hudi.common.fs.FSUtils
import org.apache.hudi.common.table.HoodieTableMetaClient
import org.apache.spark.sql.catalyst.TableIdentifier
import org.apache.spark.sql.catalyst.catalog.CatalogTypes.TablePartitionSpec
import org.apache.spark.sql.catalyst.catalog.{CatalogStatistics, CatalogTableType, HoodieCatalogTable}
import org.apache.spark.sql.hudi.HoodieSqlCommonUtils.{getPartitionPathToDrop, normalizePartitionSpec}
import org.apache.spark.sql.catalyst.catalog.{CatalogTableType, HoodieCatalogTable}
import org.apache.spark.sql.hudi.HoodieSqlCommonUtils.getPartitionPathToTruncate
import org.apache.spark.sql.{AnalysisException, Row, SparkSession}

import scala.util.control.NonFatal

/**
* Command for truncate hudi table.
*/
Expand All @@ -36,97 +31,44 @@ case class TruncateHoodieTableCommand(
partitionSpec: Option[TablePartitionSpec])
extends HoodieLeafRunnableCommand {

override def run(spark: SparkSession): Seq[Row] = {
override def run(sparkSession: SparkSession): Seq[Row] = {
val fullTableName = s"${tableIdentifier.database}.${tableIdentifier.table}"
logInfo(s"start execute truncate table command for $fullTableName")

val hoodieCatalogTable = HoodieCatalogTable(spark, tableIdentifier)
val properties = hoodieCatalogTable.tableConfig.getProps

try {
// Delete all data in the table directory
val catalog = spark.sessionState.catalog
val table = catalog.getTableMetadata(tableIdentifier)
val tableIdentWithDB = table.identifier.quotedString

if (table.tableType == CatalogTableType.VIEW) {
throw new AnalysisException(
s"Operation not allowed: TRUNCATE TABLE on views: $tableIdentWithDB")
}

if (table.partitionColumnNames.isEmpty && partitionSpec.isDefined) {
throw new AnalysisException(
s"Operation not allowed: TRUNCATE TABLE ... PARTITION is not supported " +
s"for tables that are not partitioned: $tableIdentWithDB")
}
val hoodieCatalogTable = HoodieCatalogTable(sparkSession, tableIdentifier)

val basePath = hoodieCatalogTable.tableLocation
val partCols = table.partitionColumnNames
val locations = if (partitionSpec.isEmpty || partCols.isEmpty) {
Seq(basePath)
} else {
val normalizedSpec: Seq[Map[String, String]] = Seq(partitionSpec.map { spec =>
normalizePartitionSpec(
spec,
partCols,
table.identifier.quotedString,
spark.sessionState.conf.resolver)
}.get)
val catalog = sparkSession.sessionState.catalog
val table = catalog.getTableMetadata(tableIdentifier)
val tableIdentWithDB = table.identifier.quotedString
Comment thread
xushiyan marked this conversation as resolved.
Outdated

val fullPartitionPath = FSUtils.getPartitionPath(basePath, getPartitionPathToDrop(hoodieCatalogTable, normalizedSpec))

Seq(fullPartitionPath)
}
if (table.tableType == CatalogTableType.VIEW) {
throw new AnalysisException(
s"Operation not allowed: TRUNCATE TABLE on views: $tableIdentWithDB")
}

val hadoopConf = spark.sessionState.newHadoopConf()
locations.foreach { location =>
val path = new Path(location.toString)
try {
val fs = path.getFileSystem(hadoopConf)
fs.delete(path, true)
fs.mkdirs(path)
} catch {
case NonFatal(e) =>
throw new AnalysisException(
s"Failed to truncate table $tableIdentWithDB when removing data of the path: $path " +
s"because of ${e.toString}")
}
}
if (table.partitionColumnNames.isEmpty && partitionSpec.isDefined) {
throw new AnalysisException(
s"Operation not allowed: TRUNCATE TABLE ... PARTITION is not supported " +
s"for tables that are not partitioned: $tableIdentWithDB")
}

// Also try to drop the contents of the table from the columnar cache
try {
spark.sharedState.cacheManager.uncacheQuery(spark.table(table.identifier), cascade = true)
} catch {
case NonFatal(_) =>
}
val basePath = hoodieCatalogTable.tableLocation

if (table.stats.nonEmpty) {
// empty table after truncation
val newStats = CatalogStatistics(sizeInBytes = 0, rowCount = Some(0))
catalog.alterTableStats(tableIdentifier, Some(newStats))
}
Seq.empty[Row]
} catch {
// TruncateTableCommand will delete the related directories first, and then refresh the table.
// It will fail when refresh table, because the hudi meta directory(.hoodie) has been deleted at the first step.
// So here ignore this failure, and refresh table later.
case NonFatal(e) =>
throw new AnalysisException(s"Exception when attempting to truncate table ${tableIdentifier.quotedString}: " + e)
}
val df = sparkSession.sqlContext.read
.format("hudi")
.load(basePath)

// If we have not specified the partition, truncate will delete all the data in the table path
Comment thread
xushiyan marked this conversation as resolved.
// include the hoodie.properties. In this case we should reInit the table.
if (partitionSpec.isEmpty) {
val hadoopConf = spark.sessionState.newHadoopConf()
// ReInit hoodie.properties
HoodieTableMetaClient.withPropertyBuilder()
.fromProperties(properties)
.initTable(hadoopConf, hoodieCatalogTable.tableLocation)
df.sqlContext.sql(s"delete from ${hoodieCatalogTable.tableName}")
} else {
val resolver = sparkSession.sessionState.conf.resolver
val partitionsToTruncate: String = getPartitionPathToTruncate(hoodieCatalogTable, table, partitionSpec, resolver)
df.sqlContext.sql(s"""delete from ${hoodieCatalogTable.tableName} where $partitionsToTruncate""")
}

// After deleting the data, refresh the table to make sure we don't keep around a stale
// file relation in the metastore cache and cached table data in the cache manager.
spark.catalog.refreshTable(hoodieCatalogTable.table.identifier.quotedString)
sparkSession.catalog.refreshTable(table.identifier.quotedString)
Seq.empty[Row]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ case class HoodiePostAnalysisRule(sparkSession: SparkSession) extends Rule[Logic
// Rewrite TruncateTableCommand to TruncateHoodieTableCommand
case TruncateTableCommand(tableName, partitionSpec)
if sparkAdapter.isHoodieTable(tableName, sparkSession) =>
new TruncateHoodieTableCommand(tableName, partitionSpec)
TruncateHoodieTableCommand(tableName, partitionSpec)
case _ => plan
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class TestTruncateTable extends TestHoodieSqlBase {

df.write.format("hudi")
.option(HoodieWriteConfig.TBL_NAME.key, tableName)
.option(TABLE_TYPE.key, MOR_TABLE_TYPE_OPT_VAL)
.option(TABLE_TYPE.key, COW_TABLE_TYPE_OPT_VAL)

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.

why this test case change?

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.

I wonder how the tests are succeeding? bcoz, w/ latest master, delete partitions are lazy. only after cleaner gets a chance to clean up, the deleted partition may not show up when we make getAllPartitions(). Can you check the assertions in tests. Or are we asserting for records in the deleted partitions = 0 ?

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.

why this test case change?

This case encountered this #5282 that was not covered by ut before.

.option(RECORDKEY_FIELD.key, "id")
.option(PRECOMBINE_FIELD.key, "ts")
.option(PARTITIONPATH_FIELD.key, "dt")
Expand Down