-
Notifications
You must be signed in to change notification settings - Fork 29k
[SPARK-16905] SQL DDL: MSCK REPAIR TABLE #14500
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
c5edbdf
89d22f4
f338516
7f4f38d
e478c3a
e5906cf
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 |
|---|---|---|
|
|
@@ -408,6 +408,18 @@ class SparkSqlAstBuilder(conf: SQLConf) extends AstBuilder { | |
| Option(ctx.partitionSpec).map(visitNonOptionalPartitionSpec)) | ||
| } | ||
|
|
||
| /** | ||
| * Create a [[RepairTableCommand]] command. | ||
| * | ||
| * For example: | ||
| * {{{ | ||
| * MSCK REPAIR TABLE tablename | ||
| * }}} | ||
| */ | ||
| override def visitRepairTable(ctx: RepairTableContext): LogicalPlan = withOrigin(ctx) { | ||
| RepairTableCommand(visitTableIdentifier(ctx.tableIdentifier)) | ||
|
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. Are AlterTableRecoverPartitionsCommand and RepairTableCommand the same?
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. Yes, Hive support MSCK REPAIR TABLE, EMR support ALTER TABLE RECOVER PARTITIONS
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. I see. How about we use a single command internally? |
||
| } | ||
|
|
||
| /** | ||
| * Convert a table property list into a key-value map. | ||
| * This should be called through [[visitPropertyKeyValues]] or [[visitPropertyKeys]]. | ||
|
|
@@ -778,6 +790,23 @@ class SparkSqlAstBuilder(conf: SQLConf) extends AstBuilder { | |
| ctx.PURGE != null) | ||
| } | ||
|
|
||
| /** | ||
| * Create an [[AlterTableDiscoverPartitionsCommand]] command | ||
| * | ||
| * For example: | ||
| * {{{ | ||
| * ALTER TABLE table DROP [IF EXISTS] PARTITION spec1[, PARTITION spec2, ...] [PURGE]; | ||
|
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. Nit: Update the syntax and the comments here. |
||
| * ALTER VIEW view DROP [IF EXISTS] PARTITION spec1[, PARTITION spec2, ...]; | ||
| * }}} | ||
| * | ||
| * ALTER VIEW ... DROP PARTITION ... is not supported because the concept of partitioning | ||
| * is associated with physical tables | ||
| */ | ||
| override def visitRecoverPartitions( | ||
| ctx: RecoverPartitionsContext): LogicalPlan = withOrigin(ctx) { | ||
| AlterTableRecoverPartitionsCommand(visitTableIdentifier(ctx.tableIdentifier)) | ||
| } | ||
|
|
||
| /** | ||
| * Create an [[AlterTableSetLocationCommand]] command | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,13 +17,19 @@ | |
|
|
||
| package org.apache.spark.sql.execution.command | ||
|
|
||
| import java.io.File | ||
|
|
||
| import scala.collection.GenSeq | ||
| import scala.collection.parallel.ForkJoinTaskSupport | ||
| import scala.concurrent.forkjoin.ForkJoinPool | ||
| import scala.util.control.NonFatal | ||
|
|
||
| import org.apache.hadoop.fs.{FileStatus, FileSystem, Path} | ||
|
|
||
| import org.apache.spark.sql.{AnalysisException, Row, SparkSession} | ||
| import org.apache.spark.sql.catalyst.TableIdentifier | ||
| import org.apache.spark.sql.catalyst.catalog.{BucketSpec, CatalogDatabase, CatalogTable} | ||
| import org.apache.spark.sql.catalyst.catalog.{CatalogTablePartition, CatalogTableType, SessionCatalog} | ||
| import org.apache.spark.sql.catalyst.catalog.CatalogTypes.TablePartitionSpec | ||
| import org.apache.spark.sql.catalyst.catalog.{BucketSpec, CatalogDatabase, CatalogTable, CatalogTablePartition, CatalogTableType, SessionCatalog} | ||
| import org.apache.spark.sql.catalyst.catalog.CatalogTypes._ | ||
| import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference} | ||
| import org.apache.spark.sql.execution.command.CreateDataSourceTableUtils._ | ||
| import org.apache.spark.sql.types._ | ||
|
|
@@ -425,6 +431,96 @@ case class AlterTableDropPartitionCommand( | |
|
|
||
| } | ||
|
|
||
| /** | ||
| * Discover Partitions in ALTER TABLE: discover all the partition in the directory of a table and | ||
|
||
| * update the catalog. | ||
| * | ||
| * The syntax of this command is: | ||
| * {{{ | ||
| * ALTER TABLE table DISCOVER PARTITIONS; | ||
| * }}} | ||
| */ | ||
| case class AlterTableRecoverPartitionsCommand( | ||
| tableName: TableIdentifier) extends RunnableCommand { | ||
| override def run(spark: SparkSession): Seq[Row] = { | ||
| val catalog = spark.sessionState.catalog | ||
| if (!catalog.tableExists(tableName)) { | ||
| throw new AnalysisException( | ||
| s"Table $tableName in ALTER TABLE RECOVER PARTITIONS does not exist.") | ||
| } | ||
| val table = catalog.getTableMetadata(tableName) | ||
| if (catalog.isTemporaryTable(tableName)) { | ||
| throw new AnalysisException( | ||
| s"Operation not allowed: ALTER TABLE RECOVER PARTITIONS on temporary tables: $tableName") | ||
| } | ||
| if (DDLUtils.isDatasourceTable(table)) { | ||
| throw new AnalysisException( | ||
| s"Operation not allowed: ALTER TABLE RECOVER PARTITIONS on datasource tables: $tableName") | ||
| } | ||
| if (table.tableType != CatalogTableType.EXTERNAL) { | ||
| throw new AnalysisException( | ||
| s"Operation not allowed: ALTER TABLE RECOVER PARTITIONS only works on external " + | ||
| s"tables: $tableName") | ||
| } | ||
| if (DDLUtils.isTablePartitioned(table)) { | ||
|
||
| throw new AnalysisException( | ||
| s"Operation not allowed: ALTER TABLE RECOVER PARTITIONS only works on partitioned " + | ||
| s"tables: $tableName") | ||
| } | ||
| if (table.storage.locationUri.isEmpty) { | ||
| throw new AnalysisException( | ||
| s"Operation not allowed: ALTER TABLE RECOVER PARTITIONS only works on tables with " + | ||
| s"location provided: $tableName") | ||
| } | ||
|
|
||
| recoverPartitions(spark, table) | ||
| Seq.empty[Row] | ||
| } | ||
|
|
||
| def recoverPartitions(spark: SparkSession, table: CatalogTable): Unit = { | ||
| val root = new Path(table.storage.locationUri.get) | ||
| val fs = root.getFileSystem(spark.sparkContext.hadoopConfiguration) | ||
| val partitionSpecsAndLocs = scanPartitions(spark, fs, root, Map(), table.partitionSchema.size) | ||
| val parts = partitionSpecsAndLocs.map { case (spec, location) => | ||
| // inherit table storage format (possibly except for location) | ||
| CatalogTablePartition(spec, table.storage.copy(locationUri = Some(location.toUri.toString))) | ||
| } | ||
| spark.sessionState.catalog.createPartitions(tableName, | ||
| parts.toArray[CatalogTablePartition], ignoreIfExists = true) | ||
|
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. What will happen if we get thousands of new partitions of tens thousands of new partitions?
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. Good question, see the implementation in HiveShim: All these partitions will be insert into Hive in sequential way, so group them as batches will not help here.
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. no, this is true for Hive <=0.12, for Hive 0.13+, they are sent in single RPC. so we should verify that what's limit for a single RPC
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. It seems that the Hive Metastore can't handle a RPC with millions of partitions, I will send a patch to do it in batch. |
||
| } | ||
|
|
||
| @transient private lazy val evalTaskSupport = new ForkJoinTaskSupport(new ForkJoinPool(8)) | ||
|
|
||
| private def scanPartitions( | ||
| spark: SparkSession, | ||
| fs: FileSystem, | ||
| path: Path, | ||
| spec: TablePartitionSpec, | ||
| numPartitionsLeft: Int): GenSeq[(TablePartitionSpec, Path)] = { | ||
|
||
| if (numPartitionsLeft == 0) { | ||
| return Seq(spec -> path) | ||
| } | ||
|
|
||
| val statuses = fs.listStatus(path) | ||
| val threshold = spark.conf.get("spark.rdd.parallelListingThreshold", "10").toInt | ||
| val statusPar: GenSeq[FileStatus] = | ||
| if (numPartitionsLeft > 1 && statuses.length > threshold || numPartitionsLeft > 2) { | ||
|
||
| val parArray = statuses.par | ||
|
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. i didn't look carefully - but if you are using the default exec context, please create a new one. otherwise it'd block.
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.
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. cool. can we make it explicit, e.g.
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. This is copied from UnionRDD.
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 did not figure out how it work, at least |
||
| parArray.tasksupport = evalTaskSupport | ||
| parArray | ||
| } else { | ||
| statuses | ||
| } | ||
| statusPar.flatMap { st => | ||
| val ps = st.getPath.getName.split("=", 2) | ||
| if (ps.length != 2) { | ||
| throw new AnalysisException(s"Invalid partition path: ${st.getPath}") | ||
| } | ||
| scanPartitions(spark, fs, st.getPath, spec ++ Map(ps(0) -> ps(1)), numPartitionsLeft - 1) | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * A command that sets the location of a table or a partition. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,7 @@ import org.apache.spark.sql.catalyst.catalog.CatalogTypes.TablePartitionSpec | |
| import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeReference} | ||
| import org.apache.spark.sql.catalyst.plans.logical.{Command, LogicalPlan, UnaryNode} | ||
| import org.apache.spark.sql.catalyst.util.quoteIdentifier | ||
| import org.apache.spark.sql.execution.datasources.PartitioningUtils | ||
| import org.apache.spark.sql.execution.datasources.{PartitioningUtils} | ||
| import org.apache.spark.sql.types._ | ||
| import org.apache.spark.util.Utils | ||
|
|
||
|
|
@@ -388,6 +388,50 @@ case class TruncateTableCommand( | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * A command to repair a table by discovery all the partitions in the directory. | ||
| * | ||
| * The syntax of this command is: | ||
| * {{{ | ||
| * MSCK REPAIR TABLE table_name; | ||
| * }}} | ||
| * | ||
| * This command is the same as AlterTableRecoverPartitions | ||
| */ | ||
| case class RepairTableCommand(tableName: TableIdentifier) extends RunnableCommand { | ||
|
||
| override def run(spark: SparkSession): Seq[Row] = { | ||
| val catalog = spark.sessionState.catalog | ||
| val table = catalog.getTableMetadata(tableName) | ||
| if (!catalog.tableExists(tableName)) { | ||
|
||
| throw new AnalysisException(s"Table $tableName in MSCK REPAIR TABLE does not exist.") | ||
| } | ||
| if (catalog.isTemporaryTable(tableName)) { | ||
| throw new AnalysisException( | ||
| s"Operation not allowed: MSCK REPAIR TABLE on temporary tables: $tableName") | ||
| } | ||
| if (DDLUtils.isDatasourceTable(table)) { | ||
| throw new AnalysisException( | ||
| s"Operation not allowed: MSCK REPAIR TABLE on datasource tables: $tableName") | ||
| } | ||
| if (table.tableType != CatalogTableType.EXTERNAL) { | ||
| throw new AnalysisException( | ||
| s"Operation not allowed: MSCK REPAIR TABLE only works on external tables: $tableName") | ||
| } | ||
| if (DDLUtils.isTablePartitioned(table)) { | ||
|
||
| throw new AnalysisException( | ||
| s"Operation not allowed: MSCK REPAIR TABLE only works on partitioned tables: $tableName") | ||
| } | ||
| if (table.storage.locationUri.isEmpty) { | ||
| throw new AnalysisException( | ||
| s"Operation not allowed: MSCK REPAIR TABLE only works on tables with location provided: " + | ||
|
||
| s"$tableName") | ||
| } | ||
|
|
||
| AlterTableRecoverPartitionsCommand(tableName).recoverPartitions(spark, table) | ||
| Seq.empty[Row] | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Command that looks like | ||
| * {{{ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -827,6 +827,45 @@ class DDLSuite extends QueryTest with SharedSQLContext with BeforeAndAfterEach { | |
| testAddPartitions(isDatasourceTable = true) | ||
| } | ||
|
|
||
| test("alter table: recover partitions (sequential)") { | ||
| withSQLConf("spark.rdd.parallelListingThreshold" -> "1") { | ||
| testRecoverPartitions() | ||
| } | ||
| } | ||
|
|
||
| test("after table: recover partition (parallel)") { | ||
|
||
| withSQLConf("spark.rdd.parallelListingThreshold" -> "10") { | ||
| testRecoverPartitions() | ||
| } | ||
| } | ||
|
|
||
| private def testRecoverPartitions() { | ||
| val catalog = spark.sessionState.catalog | ||
| // table to alter does not exist | ||
| intercept[AnalysisException] { | ||
| sql("ALTER TABLE does_not_exist RECOVER PARTITIONS") | ||
| } | ||
|
|
||
| val tableIdent = TableIdentifier("tab1") | ||
| createTable(catalog, tableIdent) | ||
| val part1 = Map("a" -> "1", "b" -> "5") | ||
| createTablePartition(catalog, part1, tableIdent) | ||
| assert(catalog.listPartitions(tableIdent).map(_.spec).toSet == Set(part1)) | ||
|
|
||
| val part2 = Map("a" -> "2", "b" -> "6") | ||
| val root = new Path(catalog.getTableMetadata(tableIdent).storage.locationUri.get) | ||
| val fs = root.getFileSystem(spark.sparkContext.hadoopConfiguration) | ||
| fs.mkdirs(new Path(new Path(root, "a=1"), "b=5")) | ||
| fs.mkdirs(new Path(new Path(root, "a=2"), "b=6")) | ||
| try { | ||
| sql("ALTER TABLE tab1 RECOVER PARTITIONS") | ||
| assert(catalog.listPartitions(tableIdent).map(_.spec).toSet == | ||
| Set(part1, part2)) | ||
| } finally { | ||
| fs.delete(root, true) | ||
| } | ||
| } | ||
|
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. Let's add tests to exercise the command more. Here are three examples.
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. done |
||
|
|
||
| test("alter table: add partition is not supported for views") { | ||
| assertUnsupported("ALTER VIEW dbx.tab1 ADD IF NOT EXISTS PARTITION (b='2')") | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: outline