Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@ statement
: query #statementDefault
| ctes? dmlStatementNoWith #dmlStatement
| USE NAMESPACE? multipartIdentifier #use
| CREATE database (IF NOT EXISTS)? db=errorCapturingIdentifier
| CREATE (database | NAMESPACE) (IF NOT EXISTS)? multipartIdentifier
((COMMENT comment=STRING) |
locationSpec |
(WITH DBPROPERTIES tablePropertyList))* #createDatabase
(WITH DBPROPERTIES tablePropertyList))* #createNamespace

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.

Ah I missed this. Shall we use PROPERTIES instead of DBPROPERTIES? We can write DBPROPERTIES | PROPERTIES for backward compatibility

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.

thanks for catching this. updated.

| ALTER database db=errorCapturingIdentifier
SET DBPROPERTIES tablePropertyList #setDatabaseProperties
| ALTER database db=errorCapturingIdentifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ class ResolveCatalogs(val catalogManager: CatalogManager)
s"Can not specify catalog `${catalog.name}` for view ${viewName.quoted} " +
s"because view support in catalog has not been implemented yet")

case c @ CreateNamespaceStatement(NonSessionCatalog(catalog, nameParts), _, _, _, _) =>
CreateNamespace(
catalog.asNamespaceCatalog,
nameParts,
c.ifNotExists,
c.comment,
c.locationSpec,
c.properties)

case ShowNamespacesStatement(Some(CatalogAndNamespace(catalog, namespace)), pattern) =>
ShowNamespaces(catalog.asNamespaceCatalog, namespace, pattern)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2299,6 +2299,33 @@ class AstBuilder(conf: SQLConf) extends SqlBaseBaseVisitor[AnyRef] with Logging
}
}

/**
* Create a [[CreateNamespaceStatement]] command.
*
* For example:
* {{{
* CREATE NAMESPACE [IF NOT EXISTS] ns1.ns2.ns3
* create_namespace_clauses;
*
* create_namespace_clauses (order insensitive):
* [COMMENT namespace_comment]
* [LOCATION path]
* [WITH DBPROPERTIES (key1=val1, key2=val2, ...)]
* }}}
*/
override def visitCreateNamespace(ctx: CreateNamespaceContext): LogicalPlan = withOrigin(ctx) {
checkDuplicateClauses(ctx.COMMENT, "COMMENT", ctx)
checkDuplicateClauses(ctx.locationSpec, "LOCATION", ctx)
checkDuplicateClauses(ctx.DBPROPERTIES, "WITH DBPROPERTIES", ctx)

CreateNamespaceStatement(
visitMultipartIdentifier(ctx.multipartIdentifier),
ctx.EXISTS != null,
Option(ctx.comment).map(string),
ctx.locationSpec.asScala.headOption.map(visitLocationSpec),
ctx.tablePropertyList.asScala.headOption.map(visitPropertyKeyValues).getOrElse(Map.empty))
}

/**
* Create a [[ShowNamespacesStatement]] command.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,16 @@ case class InsertIntoStatement(
case class ShowTablesStatement(namespace: Option[Seq[String]], pattern: Option[String])
extends ParsedStatement

/**
* A CREATE NAMESPACE statement, as parsed from SQL.
*/
case class CreateNamespaceStatement(
namespace: Seq[String],
Comment thread
imback82 marked this conversation as resolved.
Outdated
ifNotExists: Boolean,
comment: Option[String],
locationSpec: Option[String],
properties: Map[String, String]) extends ParsedStatement

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.

@cloud-fan, what is the behavior of IF NOT EXISTS with DBPROPERTIES? If the namespace exists, are properties updated?

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.

same as table/database, if table/database doesn't exist, the command is a noop.


/**
* A SHOW NAMESPACES statement, as parsed from SQL.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,16 @@ case class ReplaceTableAsSelect(
}
}

/**
* The logical plan of the CREATE NAMESPACE command that works for v2 catalogs.
*/
case class CreateNamespace(
catalog: SupportsNamespaces,

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.

nit: 4 space identation

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.

fixed.

namespace: Seq[String],
ifNotExists: Boolean,
comment: Option[String],
locationSpec: Option[String],
Comment thread
imback82 marked this conversation as resolved.
Outdated
properties: Map[String, String]) extends Command

/**
* The logical plan of the SHOW NAMESPACES command that works for v2 catalogs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,63 @@ class DDLParserSuite extends AnalysisTest {
ShowTablesStatement(Some(Seq("tbl")), Some("*dog*")))
}

test("create namespace") {
val sql =
"""
|CREATE DATABASE IF NOT EXISTS database_name

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.

can we use a.b.c instead of database_name?

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.

updated.

|WITH DBPROPERTIES ('a'='a', 'b'='b', 'c'='c')
|COMMENT 'database_comment' LOCATION '/home/user/db'
""".stripMargin
comparePlans(
parsePlan(sql),
CreateNamespaceStatement(
Seq("database_name"),
ifNotExists = true,
Some("database_comment"),
Some("/home/user/db"),
Map("a" -> "a", "b" -> "b", "c" -> "c")))
}

test("create namespace -- check duplicates") {
def createDatabase(duplicateClause: String): String = {
s"""
|CREATE DATABASE IF NOT EXISTS database_name
|$duplicateClause
|$duplicateClause
""".stripMargin
}
val sql1 = createDatabase("COMMENT 'database_comment'")
val sql2 = createDatabase("LOCATION '/home/user/db'")
val sql3 = createDatabase("WITH DBPROPERTIES ('a'='a', 'b'='b', 'c'='c')")

intercept(sql1, "Found duplicate clauses: COMMENT")
intercept(sql2, "Found duplicate clauses: LOCATION")
intercept(sql3, "Found duplicate clauses: WITH DBPROPERTIES")
}

test("create namespace - property values must be set") {
assertUnsupported(
sql = "CREATE DATABASE my_db WITH DBPROPERTIES('key_without_value', 'key_with_value'='x')",
containsThesePhrases = Seq("key_without_value"))
}

test("create namespace - support for other types in DBPROPERTIES") {
val sql =
"""
|CREATE DATABASE database_name
|LOCATION '/home/user/db'
|WITH DBPROPERTIES ('a'=1, 'b'=0.1, 'c'=TRUE)
""".stripMargin
comparePlans(
parsePlan(sql),
CreateNamespaceStatement(
Seq("database_name"),
ifNotExists = false,
None,
Some("/home/user/db"),
Map("a" -> "1", "b" -> "0.1", "c" -> "true")))
}

test("show databases: basic") {
comparePlans(
parsePlan("SHOW DATABASES"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class ParserUtilsSuite extends SparkFunSuite {
|WITH DBPROPERTIES ('a'='a', 'b'='b', 'c'='c')
""".stripMargin
) { parser =>
parser.statement().asInstanceOf[CreateDatabaseContext]
parser.statement().asInstanceOf[CreateNamespaceContext]
}

val emptyContext = buildContext("") { parser =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import org.apache.spark.sql.catalyst.plans.logical._
import org.apache.spark.sql.catalyst.rules.Rule
import org.apache.spark.sql.connector.catalog.{CatalogManager, CatalogPlugin, LookupCatalog, TableChange, V1Table}
import org.apache.spark.sql.connector.expressions.Transform
import org.apache.spark.sql.execution.command.{AlterTableAddColumnsCommand, AlterTableSetLocationCommand, AlterTableSetPropertiesCommand, AlterTableUnsetPropertiesCommand, AnalyzeColumnCommand, AnalyzePartitionCommand, AnalyzeTableCommand, DescribeColumnCommand, DescribeTableCommand, DropTableCommand, ShowTablesCommand}
import org.apache.spark.sql.execution.command.{AlterTableAddColumnsCommand, AlterTableSetLocationCommand, AlterTableSetPropertiesCommand, AlterTableUnsetPropertiesCommand, AnalyzeColumnCommand, AnalyzePartitionCommand, AnalyzeTableCommand, CreateDatabaseCommand, DescribeColumnCommand, DescribeTableCommand, DropTableCommand, ShowTablesCommand}
import org.apache.spark.sql.execution.datasources.{CreateTable, DataSource}
import org.apache.spark.sql.execution.datasources.v2.FileDataSourceV2
import org.apache.spark.sql.internal.SQLConf
Expand Down Expand Up @@ -255,6 +255,13 @@ class ResolveSessionCatalog(
case DropViewStatement(SessionCatalog(catalog, viewName), ifExists) =>
DropTableCommand(viewName.asTableIdentifier, ifExists, isView = true, purge = false)

case c @ CreateNamespaceStatement(SessionCatalog(catalog, nameParts), _, _, _, _) =>
if (nameParts.length != 1) {
throw new AnalysisException(
s"The database name is not valid: ${nameParts.quoted}")
}
CreateDatabaseCommand(nameParts.head, c.ifNotExists, c.locationSpec, c.comment, c.properties)

case ShowTablesStatement(Some(SessionCatalog(catalog, nameParts)), pattern) =>
if (nameParts.length != 1) {
throw new AnalysisException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,33 +374,6 @@ class SparkSqlAstBuilder(conf: SQLConf) extends AstBuilder(conf) {
"MSCK REPAIR TABLE")
}

/**
* Create a [[CreateDatabaseCommand]] command.
*
* For example:
* {{{
* CREATE DATABASE [IF NOT EXISTS] database_name
* create_database_clauses;
*
* create_database_clauses (order insensitive):
* [COMMENT database_comment]
* [LOCATION path]
* [WITH DBPROPERTIES (key1=val1, key2=val2, ...)]
* }}}
*/
override def visitCreateDatabase(ctx: CreateDatabaseContext): LogicalPlan = withOrigin(ctx) {
checkDuplicateClauses(ctx.COMMENT, "COMMENT", ctx)
checkDuplicateClauses(ctx.locationSpec, "LOCATION", ctx)
checkDuplicateClauses(ctx.DBPROPERTIES, "WITH DBPROPERTIES", ctx)

CreateDatabaseCommand(
ctx.db.getText,
ctx.EXISTS != null,
ctx.locationSpec.asScala.headOption.map(visitLocationSpec),
Option(ctx.comment).map(string),
ctx.tablePropertyList.asScala.headOption.map(visitPropertyKeyValues).getOrElse(Map.empty))
}

/**
* Create an [[AlterDatabasePropertiesCommand]] command.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.spark.sql.execution.datasources.v2

import scala.collection.JavaConverters.mapAsJavaMapConverter

import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.analysis.NamespaceAlreadyExistsException
import org.apache.spark.sql.catalyst.catalog.CatalogUtils
import org.apache.spark.sql.catalyst.expressions.Attribute
import org.apache.spark.sql.connector.catalog.SupportsNamespaces

/**
* Physical plan node for creating a namespace.
*/
case class CreateNamespaceExec(
catalog: SupportsNamespaces,
namespace: Seq[String],
ifNotExists: Boolean,
comment: Option[String],
locationSpec: Option[String],
private var properties: Map[String, String])
extends V2CommandExec {
override protected def run(): Seq[InternalRow] = {
if (ifNotExists && catalog.namespaceExists(namespace.toArray)) {
return Seq.empty
}

// Add any additional properties.
Comment thread
imback82 marked this conversation as resolved.
Outdated
if (comment.nonEmpty) {
properties += ("comment" -> comment.get)
}
if (locationSpec.nonEmpty) {
properties += ("locationSpec" -> CatalogUtils.stringToURI(locationSpec.get).toString)
}

catalog.createNamespace(namespace.toArray, properties.asJava)

Seq.empty
}

override def output: Seq[Attribute] = Seq.empty
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import scala.collection.mutable
import org.apache.spark.sql.{AnalysisException, Strategy}
import org.apache.spark.sql.catalyst.expressions.{And, AttributeReference, AttributeSet, Expression, PredicateHelper, SubqueryExpression}
import org.apache.spark.sql.catalyst.planning.PhysicalOperation
import org.apache.spark.sql.catalyst.plans.logical.{AlterTable, AppendData, CreateTableAsSelect, CreateV2Table, DeleteFromTable, DescribeTable, DropTable, LogicalPlan, OverwriteByExpression, OverwritePartitionsDynamic, Repartition, ReplaceTable, ReplaceTableAsSelect, SetCatalogAndNamespace, ShowNamespaces, ShowTables}
import org.apache.spark.sql.catalyst.plans.logical.{AlterTable, AppendData, CreateNamespace, CreateTableAsSelect, CreateV2Table, DeleteFromTable, DescribeTable, DropTable, LogicalPlan, OverwriteByExpression, OverwritePartitionsDynamic, Repartition, ReplaceTable, ReplaceTableAsSelect, SetCatalogAndNamespace, ShowNamespaces, ShowTables}
import org.apache.spark.sql.connector.catalog.{StagingTableCatalog, TableCapability}
import org.apache.spark.sql.connector.read.{Scan, ScanBuilder, SupportsPushDownFilters, SupportsPushDownRequiredColumns}
import org.apache.spark.sql.connector.read.streaming.{ContinuousStream, MicroBatchStream}
Expand Down Expand Up @@ -289,6 +289,9 @@ object DataSourceV2Strategy extends Strategy with PredicateHelper {
case AlterTable(catalog, ident, _, changes) =>
AlterTableExec(catalog, ident, changes) :: Nil

case CreateNamespace(catalog, namespace, ifNotExists, comment, locationSpec, properties) =>
CreateNamespaceExec(catalog, namespace, ifNotExists, comment, locationSpec, properties) :: Nil

case r: ShowNamespaces =>
ShowNamespacesExec(r.output, r.catalog, r.namespace, r.pattern) :: Nil

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ package org.apache.spark.sql.connector
import scala.collection.JavaConverters._

import org.apache.spark.sql._
import org.apache.spark.sql.catalyst.analysis.{CannotReplaceMissingTableException, NoSuchDatabaseException, NoSuchTableException, TableAlreadyExistsException}
import org.apache.spark.sql.catalyst.analysis.{CannotReplaceMissingTableException, NamespaceAlreadyExistsException, NoSuchDatabaseException, NoSuchTableException, TableAlreadyExistsException}
import org.apache.spark.sql.connector.catalog._
import org.apache.spark.sql.connector.catalog.CatalogManager.SESSION_CATALOG_NAME
import org.apache.spark.sql.internal.SQLConf
Expand Down Expand Up @@ -764,6 +764,32 @@ class DataSourceV2SQLSuite
assert(expected === df.collect())
}

test("CreateNameSpace: basic tests") {
// Session catalog is used.
sql("CREATE NAMESPACE ns")
testShowNamespaces("SHOW NAMESPACES", Seq("default", "ns"))

// V2 non-session catalog is used.
sql("CREATE NAMESPACE testcat.ns1.ns2")
testShowNamespaces("SHOW NAMESPACES IN testcat", Seq("ns1"))
testShowNamespaces("SHOW NAMESPACES IN testcat.ns1", Seq("ns1.ns2"))

// TODO: Add tests for validating namespace metadata when DESCRIBE NAMESPACE is available.

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 will create a JIRA for this and follow up.

}

test("CreateNameSpace: test handling of 'IF NOT EXIST'") {
sql("CREATE NAMESPACE IF NOT EXISTS testcat.ns1")

// The 'ns1' namespace already exists, so this should fail.
val exception = intercept[NamespaceAlreadyExistsException] {
sql("CREATE NAMESPACE testcat.ns1")
}
assert(exception.getMessage.contains("Namespace 'ns1' already exists"))

// The following will be no-op since the namespace already exists.
sql("CREATE NAMESPACE IF NOT EXISTS testcat.ns1")
}

test("ShowNamespaces: show root namespaces with default v2 catalog") {
spark.conf.set("spark.sql.default.catalog", "testcat")

Expand Down
Loading