voonhous commented on code in PR #19408:
URL: https://github.com/apache/hudi/pull/19408#discussion_r3690889328
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala:
##########
@@ -282,4 +284,256 @@ class TestBlobDataType extends HoodieSparkSqlTestBase {
"Expected at least one .clean instant on the timeline after
compaction")
})
}
+
+ // The following cases are parser-coverage only: a BLOB column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), which lets us exercise clauses Hudi does not
support at execution
+ // time (transform partitioning, STORED AS / ROW FORMAT, interval columns).
The BLOB column type
+ // itself proves routing because the stock Spark parser rejects the BLOB
type name.
+
+ private def parse(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("Test parse CREATE TABLE with BLOB column and primitive data types") {
+ // Exercises the primitive-data-type match arms plus NOT NULL and column
COMMENT.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_prim_tbl (
+ | c_bool BOOLEAN,
+ | c_tiny TINYINT,
+ | c_small SMALLINT,
+ | c_int INT,
+ | c_big BIGINT,
+ | c_float FLOAT,
+ | c_double DOUBLE,
+ | c_date DATE,
+ | c_str STRING,
+ | c_char CHAR(5),
+ | c_varchar VARCHAR(10),
+ | c_bin BINARY,
+ | c_dec DECIMAL,
+ | c_dec1 DECIMAL(12),
+ | c_dec2 DECIMAL(12, 3),
+ | c_notnull INT NOT NULL,
+ | c_comment INT COMMENT 'a comment',
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(BooleanType)(schema("c_bool").dataType)
+ assertResult(ByteType)(schema("c_tiny").dataType)
+ assertResult(ShortType)(schema("c_small").dataType)
+ assertResult(IntegerType)(schema("c_int").dataType)
+ assertResult(LongType)(schema("c_big").dataType)
+ assertResult(FloatType)(schema("c_float").dataType)
+ assertResult(DoubleType)(schema("c_double").dataType)
+ assertResult(DateType)(schema("c_date").dataType)
+ assertResult(StringType)(schema("c_str").dataType)
+ // CHAR/VARCHAR may be preserved or replaced with STRING depending on the
Spark version.
+ assert(Seq[DataType](CharType(5),
StringType).contains(schema("c_char").dataType))
+ assert(Seq[DataType](VarcharType(10),
StringType).contains(schema("c_varchar").dataType))
+ assertResult(BinaryType)(schema("c_bin").dataType)
+ assertResult(DecimalType(10, 0))(schema("c_dec").dataType)
+ assertResult(DecimalType(12, 0))(schema("c_dec1").dataType)
+ assertResult(DecimalType(12, 3))(schema("c_dec2").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+ assert(!schema("c_notnull").nullable)
+ assertResult("a
comment")(schema("c_comment").metadata.getString("comment"))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and complex data types") {
+ // Exercises the ARRAY / MAP / STRUCT arms and BLOB-in-struct metadata
handling.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_complex_tbl (
+ | c_arr ARRAY<INT>,
+ | c_map MAP<STRING, INT>,
+ | c_struct STRUCT<a: INT, b: STRING>,
+ | c_nested_blob STRUCT<x: BLOB>,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(ArrayType(IntegerType))(schema("c_arr").dataType)
+ assertResult(MapType(StringType, IntegerType))(schema("c_map").dataType)
+ val inner = schema("c_struct").dataType.asInstanceOf[StructType]
+ assertResult(IntegerType)(inner("a").dataType)
+ assertResult(StringType)(inner("b").dataType)
+ // A BLOB nested inside a struct still carries the BLOB type descriptor.
+ val nested = schema("c_nested_blob").dataType.asInstanceOf[StructType]("x")
+ assertResult(BlobType())(nested.dataType)
+ assertResult(HoodieSchemaType.BLOB.name())(
+ nested.metadata.getString(HoodieSchema.TYPE_METADATA_FIELD))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and interval data types") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_ivl_tbl (
+ | i_year INTERVAL YEAR,
+ | i_ym INTERVAL YEAR TO MONTH,
+ | i_day INTERVAL DAY,
+ | i_ds INTERVAL DAY TO SECOND,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+
assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR))(schema("i_year").dataType)
+ assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH))(
+ schema("i_ym").dataType)
+
assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY))(schema("i_day").dataType)
+ assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY,
DayTimeIntervalType.SECOND))(
+ schema("i_ds").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+
+ // Endpoints where the end field does not follow the start are rejected by
both interval
+ // data-type visitors. The grammar only allows YEAR/MONTH -> MONTH and
DAY/HOUR/MINUTE/SECOND
+ // -> HOUR/MINUTE/SECOND, so these stay grammatical yet still hit the
builder's end <= start guard.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_ym (id BIGINT, bad INTERVAL MONTH TO MONTH, data
BLOB) USING hudi")(
+ "are not supported")
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_dt (id BIGINT, bad INTERVAL SECOND TO HOUR, data
BLOB) USING hudi")(
+ "are not supported")
+
+ // An unknown primitive type name is rejected.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_type (id BIGINT, weird sometype, data BLOB) USING
hudi")(
+ "is not supported")
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and partition transforms") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_tf_tbl (
+ | id BIGINT,
+ | ts DATE,
+ | region STRING,
+ | data BLOB
+ |) USING hudi
+ |PARTITIONED BY (region, years(ts), months(ts), days(ts), hours(ts),
myfunc(id))
+ """.stripMargin)
+ assertResult(BlobType())(plan.tableSchema("data").dataType)
+ assertResult(Seq(Seq("region")))(transformFieldRefs(transformByName(plan,
"identity")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"years")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"months")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"days")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"hours")))
+ // an arbitrary function transform falls through to the generic
apply-transform arm
+ assertResult(Seq(Seq("id")))(transformFieldRefs(transformByName(plan,
"myfunc")))
+
+ // bucket(numBuckets, col) with int, long and short number-of-buckets
literals exercises the
+ // three numeric arms of the bucket handling.
+ Seq("4", "4L", "4S").foreach { numLiteral =>
+ val bp = parse(
+ s"CREATE TABLE blob_bkt_tbl (id BIGINT, data BLOB) USING hudi " +
+ s"PARTITIONED BY (bucket($numLiteral, id))")
+ val bkt = transformByName(bp, "bucket")
+ assertResult("4")(firstLiteralArg(bkt).value.toString)
+ assertResult(Seq(Seq("id")))(transformFieldRefs(bkt))
+ }
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and typed transform-argument
literals") {
+ // Constant transform arguments exercise the literal visitors: string,
integer, big-integer and
+ // exponent numerics (the private numeric-literal helper), the typed date
constructor, and both
+ // interval forms (multi-unit and unit-to-unit). A typed timestamp
constructor is not used
+ // because the Spark 4.x extended parser rejects a bare TIMESTAMP token.
Note: a bare
Review Comment:
The root cause here is not the Spark 4.x parser -- it is Hudi's wiring. All
six `HoodieSpark*ExtendedSqlParser` set `parser.SQL_standard_keyword_behavior =
conf.ansiEnabled`, while Spark's own parser sets it from
`conf.enforceReservedKeywords` (`spark.sql.ansi.enforceReservedKeywords`,
default **false** even under ANSI). Spark 4.x defaults `ansi.enabled=true`, so
the fork enforces reserved keywords that stock Spark does not, and `CREATE
TABLE t (ts TIMESTAMP, data BLOB) USING hudi` fails with `no viable alternative
at input 'TIMESTAMP'` while the same statement minus the BLOB column parses
fine. Verified the 3.5 and regenerated 4.1 forked grammars behave identically
once the flag matches; the blast radius also includes common column names
(`user`, `left`, `filter`, `some`) in any blob/vector statement.
Action: please file a GitHub issue for the one-line fix (use
`conf.enforceReservedKeywords` in the six parsers) and reword this comment to
cite it as a tracked bug, not a Spark 4 constraint.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala:
##########
@@ -282,4 +284,256 @@ class TestBlobDataType extends HoodieSparkSqlTestBase {
"Expected at least one .clean instant on the timeline after
compaction")
})
}
+
+ // The following cases are parser-coverage only: a BLOB column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), which lets us exercise clauses Hudi does not
support at execution
+ // time (transform partitioning, STORED AS / ROW FORMAT, interval columns).
The BLOB column type
+ // itself proves routing because the stock Spark parser rejects the BLOB
type name.
+
+ private def parse(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("Test parse CREATE TABLE with BLOB column and primitive data types") {
+ // Exercises the primitive-data-type match arms plus NOT NULL and column
COMMENT.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_prim_tbl (
+ | c_bool BOOLEAN,
+ | c_tiny TINYINT,
+ | c_small SMALLINT,
+ | c_int INT,
+ | c_big BIGINT,
+ | c_float FLOAT,
+ | c_double DOUBLE,
+ | c_date DATE,
+ | c_str STRING,
+ | c_char CHAR(5),
+ | c_varchar VARCHAR(10),
+ | c_bin BINARY,
+ | c_dec DECIMAL,
+ | c_dec1 DECIMAL(12),
+ | c_dec2 DECIMAL(12, 3),
+ | c_notnull INT NOT NULL,
+ | c_comment INT COMMENT 'a comment',
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(BooleanType)(schema("c_bool").dataType)
+ assertResult(ByteType)(schema("c_tiny").dataType)
+ assertResult(ShortType)(schema("c_small").dataType)
+ assertResult(IntegerType)(schema("c_int").dataType)
+ assertResult(LongType)(schema("c_big").dataType)
+ assertResult(FloatType)(schema("c_float").dataType)
+ assertResult(DoubleType)(schema("c_double").dataType)
+ assertResult(DateType)(schema("c_date").dataType)
+ assertResult(StringType)(schema("c_str").dataType)
+ // CHAR/VARCHAR may be preserved or replaced with STRING depending on the
Spark version.
+ assert(Seq[DataType](CharType(5),
StringType).contains(schema("c_char").dataType))
+ assert(Seq[DataType](VarcharType(10),
StringType).contains(schema("c_varchar").dataType))
+ assertResult(BinaryType)(schema("c_bin").dataType)
+ assertResult(DecimalType(10, 0))(schema("c_dec").dataType)
+ assertResult(DecimalType(12, 0))(schema("c_dec1").dataType)
+ assertResult(DecimalType(12, 3))(schema("c_dec2").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+ assert(!schema("c_notnull").nullable)
+ assertResult("a
comment")(schema("c_comment").metadata.getString("comment"))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and complex data types") {
+ // Exercises the ARRAY / MAP / STRUCT arms and BLOB-in-struct metadata
handling.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_complex_tbl (
+ | c_arr ARRAY<INT>,
+ | c_map MAP<STRING, INT>,
+ | c_struct STRUCT<a: INT, b: STRING>,
+ | c_nested_blob STRUCT<x: BLOB>,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(ArrayType(IntegerType))(schema("c_arr").dataType)
+ assertResult(MapType(StringType, IntegerType))(schema("c_map").dataType)
+ val inner = schema("c_struct").dataType.asInstanceOf[StructType]
+ assertResult(IntegerType)(inner("a").dataType)
+ assertResult(StringType)(inner("b").dataType)
+ // A BLOB nested inside a struct still carries the BLOB type descriptor.
+ val nested = schema("c_nested_blob").dataType.asInstanceOf[StructType]("x")
+ assertResult(BlobType())(nested.dataType)
+ assertResult(HoodieSchemaType.BLOB.name())(
+ nested.metadata.getString(HoodieSchema.TYPE_METADATA_FIELD))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and interval data types") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_ivl_tbl (
+ | i_year INTERVAL YEAR,
+ | i_ym INTERVAL YEAR TO MONTH,
+ | i_day INTERVAL DAY,
+ | i_ds INTERVAL DAY TO SECOND,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+
assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR))(schema("i_year").dataType)
+ assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH))(
+ schema("i_ym").dataType)
+
assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY))(schema("i_day").dataType)
+ assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY,
DayTimeIntervalType.SECOND))(
+ schema("i_ds").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+
+ // Endpoints where the end field does not follow the start are rejected by
both interval
+ // data-type visitors. The grammar only allows YEAR/MONTH -> MONTH and
DAY/HOUR/MINUTE/SECOND
+ // -> HOUR/MINUTE/SECOND, so these stay grammatical yet still hit the
builder's end <= start guard.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_ym (id BIGINT, bad INTERVAL MONTH TO MONTH, data
BLOB) USING hudi")(
+ "are not supported")
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_dt (id BIGINT, bad INTERVAL SECOND TO HOUR, data
BLOB) USING hudi")(
+ "are not supported")
+
+ // An unknown primitive type name is rejected.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_type (id BIGINT, weird sometype, data BLOB) USING
hudi")(
Review Comment:
This arm is already covered: a bare `v VECTOR` is `("vector", Nil)` and
falls into the same `case (dt, params) => throw ... "DataType $dtStr is not
supported."` arm, asserted by `test create table with VECTOR without dimension
fails` in `TestCreateTable`. And `"is not supported"` is generic enough to
match almost any error text.
Action: drop these three lines; if you prefer to keep them, tighten the
assertion to `"DataType sometype is not supported"`.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala:
##########
@@ -282,4 +284,256 @@ class TestBlobDataType extends HoodieSparkSqlTestBase {
"Expected at least one .clean instant on the timeline after
compaction")
})
}
+
+ // The following cases are parser-coverage only: a BLOB column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), which lets us exercise clauses Hudi does not
support at execution
+ // time (transform partitioning, STORED AS / ROW FORMAT, interval columns).
The BLOB column type
+ // itself proves routing because the stock Spark parser rejects the BLOB
type name.
+
+ private def parse(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("Test parse CREATE TABLE with BLOB column and primitive data types") {
+ // Exercises the primitive-data-type match arms plus NOT NULL and column
COMMENT.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_prim_tbl (
+ | c_bool BOOLEAN,
+ | c_tiny TINYINT,
+ | c_small SMALLINT,
+ | c_int INT,
+ | c_big BIGINT,
+ | c_float FLOAT,
+ | c_double DOUBLE,
+ | c_date DATE,
+ | c_str STRING,
+ | c_char CHAR(5),
+ | c_varchar VARCHAR(10),
+ | c_bin BINARY,
+ | c_dec DECIMAL,
+ | c_dec1 DECIMAL(12),
+ | c_dec2 DECIMAL(12, 3),
+ | c_notnull INT NOT NULL,
+ | c_comment INT COMMENT 'a comment',
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(BooleanType)(schema("c_bool").dataType)
+ assertResult(ByteType)(schema("c_tiny").dataType)
+ assertResult(ShortType)(schema("c_small").dataType)
+ assertResult(IntegerType)(schema("c_int").dataType)
+ assertResult(LongType)(schema("c_big").dataType)
+ assertResult(FloatType)(schema("c_float").dataType)
+ assertResult(DoubleType)(schema("c_double").dataType)
+ assertResult(DateType)(schema("c_date").dataType)
+ assertResult(StringType)(schema("c_str").dataType)
+ // CHAR/VARCHAR may be preserved or replaced with STRING depending on the
Spark version.
+ assert(Seq[DataType](CharType(5),
StringType).contains(schema("c_char").dataType))
+ assert(Seq[DataType](VarcharType(10),
StringType).contains(schema("c_varchar").dataType))
+ assertResult(BinaryType)(schema("c_bin").dataType)
+ assertResult(DecimalType(10, 0))(schema("c_dec").dataType)
+ assertResult(DecimalType(12, 0))(schema("c_dec1").dataType)
+ assertResult(DecimalType(12, 3))(schema("c_dec2").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+ assert(!schema("c_notnull").nullable)
+ assertResult("a
comment")(schema("c_comment").metadata.getString("comment"))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and complex data types") {
+ // Exercises the ARRAY / MAP / STRUCT arms and BLOB-in-struct metadata
handling.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_complex_tbl (
+ | c_arr ARRAY<INT>,
+ | c_map MAP<STRING, INT>,
+ | c_struct STRUCT<a: INT, b: STRING>,
+ | c_nested_blob STRUCT<x: BLOB>,
Review Comment:
nit, feel free to ignore: `c_nested_blob` duplicates existing coverage --
`test BLOB in nested struct` in `TestCreateTable` (~line 2117) already asserts
both `BlobType()` and `TYPE_METADATA_FIELD == BLOB` for a `STRUCT` field
through the same builder path. The ARRAY arm is the genuinely new part of this
test. Consider dropping the field and its two assertions.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala:
##########
@@ -282,4 +284,256 @@ class TestBlobDataType extends HoodieSparkSqlTestBase {
"Expected at least one .clean instant on the timeline after
compaction")
})
}
+
+ // The following cases are parser-coverage only: a BLOB column routes the
whole CREATE TABLE
Review Comment:
The PR description's "every remaining path is reachable ... no further prune
is warranted" does not hold. Post-#19132 the six builders still carry provably
dead code: `createSchema`, `createStructType`, both `visitPropertyKeys`
overloads (only referenced from scaladoc), `visitBooleanLiteral` (unreachable
in both keyword modes), grammar-guaranteed-unreachable branches
(`getSingleFieldReference` empty-args arm, `visitTransformArgument.getOrElse`,
`visitCreateFileFormat` `case _`, `validateRowFormatFileFormat` `case _`), and
~8 unused imports per builder.
Action: delete these in a small follow-up (or a cleanup commit here) and
adjust the PR description claim.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala:
##########
@@ -2340,6 +2341,140 @@ class TestCreateTable extends HoodieSparkSqlTestBase {
}
}
+ // The following cases are parser-coverage only: a VECTOR column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), matching how TestIndexSyntax exercises the
index statements, which
+ // lets us cover clauses that are not supported at execution time (transform
partitioning,
+ // CLUSTERED BY, typed literal arguments). The VECTOR column type proves the
statement routed
+ // here because the stock Spark parser rejects the VECTOR type name.
+
+ private def parseCreateTable(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("test create VECTOR table with partition transforms parses (parser
coverage)") {
Review Comment:
These two tests (this one and the typed-literals one below) exercise exactly
the production lines their BLOB twins in `TestBlobDataType` already cover: the
builder branches on blob-vs-vector only in `visitPrimitiveDataType` (3.5
builder lines 572-573); every clause visitor is type-agnostic. The literals
test is a line-for-line copy (same seven literal forms -- the PR description's
claim that the VECTOR one adds long/exponent/interval variants does not hold,
the BLOB copy has them all), and this transforms test is a strict subset:
`truncate` has no dedicated arm (`grep 'case "truncate"'` over the six builders
returns nothing), so it lands in the same generic-apply case as `myfunc`. Same
shape that got `TestExtendedSqlParserCoverage` removed in the #19218 review.
Action: drop both tests (lines 2354-2419) and keep the BLOB copies; if you
want a VECTOR routing smoke check, a single bucket assertion inside the
CLUSTERED BY test below is enough.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala:
##########
@@ -2340,6 +2341,140 @@ class TestCreateTable extends HoodieSparkSqlTestBase {
}
}
+ // The following cases are parser-coverage only: a VECTOR column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), matching how TestIndexSyntax exercises the
index statements, which
+ // lets us cover clauses that are not supported at execution time (transform
partitioning,
+ // CLUSTERED BY, typed literal arguments). The VECTOR column type proves the
statement routed
+ // here because the stock Spark parser rejects the VECTOR type name.
+
+ private def parseCreateTable(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("test create VECTOR table with partition transforms parses (parser
coverage)") {
+ val plan = parseCreateTable(
+ s"""
+ |CREATE TABLE vec_parse_tbl (
+ | id BIGINT,
+ | ts DATE,
+ | region STRING,
+ | name STRING,
+ | embedding VECTOR(8)
+ |) USING hudi
+ |PARTITIONED BY (region, bucket(4, id), years(ts), truncate(10, name))
+ """.stripMargin)
+
+ assertEquals(ArrayType(FloatType, containsNull = false),
plan.tableSchema("embedding").dataType)
+
+ assertEquals(Seq(Seq("region")), transformFieldRefs(transformByName(plan,
"identity")))
+ val bucket = transformByName(plan, "bucket")
+ assertEquals(IntegerType, firstLiteralArg(bucket).dataType)
+ assertEquals("4", firstLiteralArg(bucket).value.toString)
+ assertEquals(Seq(Seq("id")), transformFieldRefs(bucket))
+ assertEquals(Seq(Seq("ts")), transformFieldRefs(transformByName(plan,
"years")))
+ val truncate = transformByName(plan, "truncate")
+ assertEquals(IntegerType, firstLiteralArg(truncate).dataType)
+ assertEquals("10", firstLiteralArg(truncate).value.toString)
+ assertEquals(Seq(Seq("name")), transformFieldRefs(truncate))
+ }
+
+ test("test create VECTOR table with typed transform-argument literals parses
(parser coverage)") {
+ // Each transform carries a differently-typed constant argument to
exercise the literal
+ // visitors: string, integer, long, exponent/double, the typed date
constructor, and both
+ // interval forms (multi-units and unit-to-unit). A typed timestamp
constructor is not used
+ // because the Spark 4.x extended parser rejects a bare TIMESTAMP token. A
bare true/false/null
+ // in this position is parsed as a column reference (qualifiedName takes
precedence over a
+ // constant in the grammar under the default non-ANSI config), so the
boolean and null literal
+ // visitors are not reachable from a CREATE TABLE statement.
+ val plan = parseCreateTable(
+ s"""
+ |CREATE TABLE vec_lit_tbl (
+ | id BIGINT,
+ | embedding VECTOR(4)
+ |) USING hudi
+ |PARTITIONED BY (
+ | str_t('x', id),
+ | int_t(4, id),
+ | long_t(9000000000L, id),
+ | exp_t(1E3, id),
+ | date_t(DATE '2020-01-01', id),
+ | mu_ivl_t(INTERVAL '1' DAY, id),
+ | uu_ivl_t(INTERVAL '1-2' YEAR TO MONTH, id)
+ |)
+ """.stripMargin)
+
+ assertEquals(StringType, firstLiteralArg(transformByName(plan,
"str_t")).dataType)
+ assertEquals("x", firstLiteralArg(transformByName(plan,
"str_t")).value.toString)
+ assertEquals(IntegerType, firstLiteralArg(transformByName(plan,
"int_t")).dataType)
+ assertEquals(LongType, firstLiteralArg(transformByName(plan,
"long_t")).dataType)
+ assertEquals(DoubleType, firstLiteralArg(transformByName(plan,
"exp_t")).dataType)
+ assertEquals(DateType, firstLiteralArg(transformByName(plan,
"date_t")).dataType)
+ assertEquals(
+ DayTimeIntervalType(DayTimeIntervalType.DAY, DayTimeIntervalType.DAY),
+ firstLiteralArg(transformByName(plan, "mu_ivl_t")).dataType)
+ assertEquals(
+ YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH),
+ firstLiteralArg(transformByName(plan, "uu_ivl_t")).dataType)
+ }
+
+ test("test create VECTOR table with CLUSTERED BY bucket spec parses (parser
coverage)") {
+ val plan = parseCreateTable(
+ "CREATE TABLE vec_bucket_tbl (id BIGINT, embedding VECTOR(4)) USING hudi
" +
+ "CLUSTERED BY (id) INTO 4 BUCKETS")
+ val bucket = transformByName(plan, "bucket")
+ assertEquals("4", firstLiteralArg(bucket).value.toString)
+ assertEquals(Seq(Seq("id")), transformFieldRefs(bucket))
+
+ // SORTED BY ... ASC is accepted by the bucket-spec visitor.
+ val sortedPlan = parseCreateTable(
+ "CREATE TABLE vec_sbucket_tbl (id BIGINT, ts BIGINT, embedding
VECTOR(4)) USING hudi " +
+ "CLUSTERED BY (id, ts) SORTED BY (id ASC) INTO 8 BUCKETS")
+
assertTrue(sortedPlan.partitioning.exists(_.name.toLowerCase.contains("bucket")))
+
+ // SORTED BY ... DESC is rejected by the bucket-spec visitor.
+ checkExceptionContain(
+ "CREATE TABLE vec_bad_bucket_tbl (id BIGINT, embedding VECTOR(4)) USING
hudi " +
+ "CLUSTERED BY (id) SORTED BY (id DESC) INTO 4 BUCKETS")(
+ "Column ordering must be ASC")
+ }
+
+ test("test create VECTOR table with LOCATION/COMMENT/OPTIONS/TBLPROPERTIES
parses (parser coverage)") {
Review Comment:
Neither positive case asserts any of the four clauses in the test name --
both check only `embedding`'s dataType, so a builder that dropped
LOCATION/COMMENT/OPTIONS/TBLPROPERTIES entirely would pass, and the "path
folded into location" behavior claimed below is never asserted. The fields are
on the plan and portable across all six profiles (3.3/3.4 `TableSpec`, 3.5+
`TableSpecBase`: `location`/`comment`/`properties`; note `.options` is **not**
on `TableSpecBase`, so avoid it).
Action: assert `tableSpec.comment == Some("a vector table")`,
`tableSpec.location == Some("/tmp/vec_clause_tbl")`, the three `prop_*` entries
in `tableSpec.properties`, and `pathPlan.tableSpec.location ==
Some("/tmp/vec_path_tbl")` for the folding case.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala:
##########
@@ -282,4 +284,256 @@ class TestBlobDataType extends HoodieSparkSqlTestBase {
"Expected at least one .clean instant on the timeline after
compaction")
})
}
+
+ // The following cases are parser-coverage only: a BLOB column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), which lets us exercise clauses Hudi does not
support at execution
+ // time (transform partitioning, STORED AS / ROW FORMAT, interval columns).
The BLOB column type
+ // itself proves routing because the stock Spark parser rejects the BLOB
type name.
+
+ private def parse(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("Test parse CREATE TABLE with BLOB column and primitive data types") {
+ // Exercises the primitive-data-type match arms plus NOT NULL and column
COMMENT.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_prim_tbl (
+ | c_bool BOOLEAN,
+ | c_tiny TINYINT,
+ | c_small SMALLINT,
+ | c_int INT,
+ | c_big BIGINT,
+ | c_float FLOAT,
+ | c_double DOUBLE,
+ | c_date DATE,
+ | c_str STRING,
+ | c_char CHAR(5),
+ | c_varchar VARCHAR(10),
+ | c_bin BINARY,
+ | c_dec DECIMAL,
+ | c_dec1 DECIMAL(12),
+ | c_dec2 DECIMAL(12, 3),
+ | c_notnull INT NOT NULL,
+ | c_comment INT COMMENT 'a comment',
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(BooleanType)(schema("c_bool").dataType)
+ assertResult(ByteType)(schema("c_tiny").dataType)
+ assertResult(ShortType)(schema("c_small").dataType)
+ assertResult(IntegerType)(schema("c_int").dataType)
+ assertResult(LongType)(schema("c_big").dataType)
+ assertResult(FloatType)(schema("c_float").dataType)
+ assertResult(DoubleType)(schema("c_double").dataType)
+ assertResult(DateType)(schema("c_date").dataType)
+ assertResult(StringType)(schema("c_str").dataType)
+ // CHAR/VARCHAR may be preserved or replaced with STRING depending on the
Spark version.
+ assert(Seq[DataType](CharType(5),
StringType).contains(schema("c_char").dataType))
+ assert(Seq[DataType](VarcharType(10),
StringType).contains(schema("c_varchar").dataType))
+ assertResult(BinaryType)(schema("c_bin").dataType)
+ assertResult(DecimalType(10, 0))(schema("c_dec").dataType)
+ assertResult(DecimalType(12, 0))(schema("c_dec1").dataType)
+ assertResult(DecimalType(12, 3))(schema("c_dec2").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+ assert(!schema("c_notnull").nullable)
+ assertResult("a
comment")(schema("c_comment").metadata.getString("comment"))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and complex data types") {
+ // Exercises the ARRAY / MAP / STRUCT arms and BLOB-in-struct metadata
handling.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_complex_tbl (
+ | c_arr ARRAY<INT>,
+ | c_map MAP<STRING, INT>,
+ | c_struct STRUCT<a: INT, b: STRING>,
+ | c_nested_blob STRUCT<x: BLOB>,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(ArrayType(IntegerType))(schema("c_arr").dataType)
+ assertResult(MapType(StringType, IntegerType))(schema("c_map").dataType)
+ val inner = schema("c_struct").dataType.asInstanceOf[StructType]
+ assertResult(IntegerType)(inner("a").dataType)
+ assertResult(StringType)(inner("b").dataType)
+ // A BLOB nested inside a struct still carries the BLOB type descriptor.
+ val nested = schema("c_nested_blob").dataType.asInstanceOf[StructType]("x")
+ assertResult(BlobType())(nested.dataType)
+ assertResult(HoodieSchemaType.BLOB.name())(
+ nested.metadata.getString(HoodieSchema.TYPE_METADATA_FIELD))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and interval data types") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_ivl_tbl (
+ | i_year INTERVAL YEAR,
+ | i_ym INTERVAL YEAR TO MONTH,
+ | i_day INTERVAL DAY,
+ | i_ds INTERVAL DAY TO SECOND,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+
assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR))(schema("i_year").dataType)
+ assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH))(
+ schema("i_ym").dataType)
+
assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY))(schema("i_day").dataType)
+ assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY,
DayTimeIntervalType.SECOND))(
+ schema("i_ds").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+
+ // Endpoints where the end field does not follow the start are rejected by
both interval
+ // data-type visitors. The grammar only allows YEAR/MONTH -> MONTH and
DAY/HOUR/MINUTE/SECOND
+ // -> HOUR/MINUTE/SECOND, so these stay grammatical yet still hit the
builder's end <= start guard.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_ym (id BIGINT, bad INTERVAL MONTH TO MONTH, data
BLOB) USING hudi")(
+ "are not supported")
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_dt (id BIGINT, bad INTERVAL SECOND TO HOUR, data
BLOB) USING hudi")(
+ "are not supported")
+
+ // An unknown primitive type name is rejected.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_type (id BIGINT, weird sometype, data BLOB) USING
hudi")(
+ "is not supported")
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and partition transforms") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_tf_tbl (
+ | id BIGINT,
+ | ts DATE,
+ | region STRING,
+ | data BLOB
+ |) USING hudi
+ |PARTITIONED BY (region, years(ts), months(ts), days(ts), hours(ts),
myfunc(id))
+ """.stripMargin)
+ assertResult(BlobType())(plan.tableSchema("data").dataType)
+ assertResult(Seq(Seq("region")))(transformFieldRefs(transformByName(plan,
"identity")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"years")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"months")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"days")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"hours")))
+ // an arbitrary function transform falls through to the generic
apply-transform arm
+ assertResult(Seq(Seq("id")))(transformFieldRefs(transformByName(plan,
"myfunc")))
+
+ // bucket(numBuckets, col) with int, long and short number-of-buckets
literals exercises the
+ // three numeric arms of the bucket handling.
+ Seq("4", "4L", "4S").foreach { numLiteral =>
+ val bp = parse(
+ s"CREATE TABLE blob_bkt_tbl (id BIGINT, data BLOB) USING hudi " +
+ s"PARTITIONED BY (bucket($numLiteral, id))")
+ val bkt = transformByName(bp, "bucket")
+ assertResult("4")(firstLiteralArg(bkt).value.toString)
+ assertResult(Seq(Seq("id")))(transformFieldRefs(bkt))
+ }
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and typed transform-argument
literals") {
+ // Constant transform arguments exercise the literal visitors: string,
integer, big-integer and
+ // exponent numerics (the private numeric-literal helper), the typed date
constructor, and both
+ // interval forms (multi-unit and unit-to-unit). A typed timestamp
constructor is not used
+ // because the Spark 4.x extended parser rejects a bare TIMESTAMP token.
Note: a bare
+ // true/false/null in this position is parsed as a column reference
(qualifiedName takes
+ // precedence over a constant in the grammar under the default non-ANSI
config), so the boolean
+ // and null literal visitors are not reachable from a CREATE TABLE
statement.
Review Comment:
Half of this claim only holds under non-ANSI keyword mode. With the current
wiring (`SQL_standard_keyword_behavior = conf.ansiEnabled`), the Spark 4.x
profiles run with the flag on, and there `f(null, id)` parses as a null
literal, so `visitNullLiteral` **is** reachable on 4.x. Only
`visitBooleanLiteral` is unreachable in both modes (`TRUE`/`FALSE` are in
`ansiNonReserved`, so `qualifiedName` always wins) -- and the `OPTIONS ('k' =
true)` case does not reach it either, since `visitPropertyValue` reads
`booleanValue` via `getText` without visiting the constant.
Action: reword to "`null` is a column reference under non-ANSI keyword mode
and a literal under ANSI (the Spark 4.x default)". `visitBooleanLiteral` is
provably dead in the six builders and can be deleted in a follow-up instead of
documented.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala:
##########
@@ -282,4 +284,256 @@ class TestBlobDataType extends HoodieSparkSqlTestBase {
"Expected at least one .clean instant on the timeline after
compaction")
})
}
+
+ // The following cases are parser-coverage only: a BLOB column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), which lets us exercise clauses Hudi does not
support at execution
+ // time (transform partitioning, STORED AS / ROW FORMAT, interval columns).
The BLOB column type
+ // itself proves routing because the stock Spark parser rejects the BLOB
type name.
+
+ private def parse(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("Test parse CREATE TABLE with BLOB column and primitive data types") {
+ // Exercises the primitive-data-type match arms plus NOT NULL and column
COMMENT.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_prim_tbl (
+ | c_bool BOOLEAN,
+ | c_tiny TINYINT,
+ | c_small SMALLINT,
+ | c_int INT,
+ | c_big BIGINT,
+ | c_float FLOAT,
+ | c_double DOUBLE,
+ | c_date DATE,
+ | c_str STRING,
+ | c_char CHAR(5),
+ | c_varchar VARCHAR(10),
+ | c_bin BINARY,
+ | c_dec DECIMAL,
+ | c_dec1 DECIMAL(12),
+ | c_dec2 DECIMAL(12, 3),
+ | c_notnull INT NOT NULL,
+ | c_comment INT COMMENT 'a comment',
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(BooleanType)(schema("c_bool").dataType)
+ assertResult(ByteType)(schema("c_tiny").dataType)
+ assertResult(ShortType)(schema("c_small").dataType)
+ assertResult(IntegerType)(schema("c_int").dataType)
+ assertResult(LongType)(schema("c_big").dataType)
+ assertResult(FloatType)(schema("c_float").dataType)
+ assertResult(DoubleType)(schema("c_double").dataType)
+ assertResult(DateType)(schema("c_date").dataType)
+ assertResult(StringType)(schema("c_str").dataType)
+ // CHAR/VARCHAR may be preserved or replaced with STRING depending on the
Spark version.
+ assert(Seq[DataType](CharType(5),
StringType).contains(schema("c_char").dataType))
+ assert(Seq[DataType](VarcharType(10),
StringType).contains(schema("c_varchar").dataType))
+ assertResult(BinaryType)(schema("c_bin").dataType)
+ assertResult(DecimalType(10, 0))(schema("c_dec").dataType)
+ assertResult(DecimalType(12, 0))(schema("c_dec1").dataType)
+ assertResult(DecimalType(12, 3))(schema("c_dec2").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+ assert(!schema("c_notnull").nullable)
+ assertResult("a
comment")(schema("c_comment").metadata.getString("comment"))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and complex data types") {
+ // Exercises the ARRAY / MAP / STRUCT arms and BLOB-in-struct metadata
handling.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_complex_tbl (
+ | c_arr ARRAY<INT>,
+ | c_map MAP<STRING, INT>,
+ | c_struct STRUCT<a: INT, b: STRING>,
+ | c_nested_blob STRUCT<x: BLOB>,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(ArrayType(IntegerType))(schema("c_arr").dataType)
+ assertResult(MapType(StringType, IntegerType))(schema("c_map").dataType)
+ val inner = schema("c_struct").dataType.asInstanceOf[StructType]
+ assertResult(IntegerType)(inner("a").dataType)
+ assertResult(StringType)(inner("b").dataType)
+ // A BLOB nested inside a struct still carries the BLOB type descriptor.
+ val nested = schema("c_nested_blob").dataType.asInstanceOf[StructType]("x")
+ assertResult(BlobType())(nested.dataType)
+ assertResult(HoodieSchemaType.BLOB.name())(
+ nested.metadata.getString(HoodieSchema.TYPE_METADATA_FIELD))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and interval data types") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_ivl_tbl (
+ | i_year INTERVAL YEAR,
+ | i_ym INTERVAL YEAR TO MONTH,
+ | i_day INTERVAL DAY,
+ | i_ds INTERVAL DAY TO SECOND,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+
assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR))(schema("i_year").dataType)
+ assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH))(
+ schema("i_ym").dataType)
+
assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY))(schema("i_day").dataType)
+ assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY,
DayTimeIntervalType.SECOND))(
+ schema("i_ds").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+
+ // Endpoints where the end field does not follow the start are rejected by
both interval
+ // data-type visitors. The grammar only allows YEAR/MONTH -> MONTH and
DAY/HOUR/MINUTE/SECOND
+ // -> HOUR/MINUTE/SECOND, so these stay grammatical yet still hit the
builder's end <= start guard.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_ym (id BIGINT, bad INTERVAL MONTH TO MONTH, data
BLOB) USING hudi")(
+ "are not supported")
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_dt (id BIGINT, bad INTERVAL SECOND TO HOUR, data
BLOB) USING hudi")(
+ "are not supported")
+
+ // An unknown primitive type name is rejected.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_type (id BIGINT, weird sometype, data BLOB) USING
hudi")(
+ "is not supported")
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and partition transforms") {
Review Comment:
Coverage gap worth closing while you are here: none of the transform tests
hits the partition-**column** arm (`PARTITIONED BY (p STRING)`), which is the
only untested branch that changes the emitted schema (`visitCreateTable`
appends `partCols` to the columns). Cheap negatives in the same area with zero
coverage today: the mixed form `PARTITIONED BY (p STRING, bucket(4, id))`,
`SKEWED BY`, and `CREATE TEMPORARY TABLE` (all reach the builder's mix-error /
`operationNotAllowed` paths).
Action: add a `PARTITIONED BY (p STRING)` case asserting `p` appears in
`tableSchema`, plus the three negatives.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala:
##########
@@ -282,4 +284,256 @@ class TestBlobDataType extends HoodieSparkSqlTestBase {
"Expected at least one .clean instant on the timeline after
compaction")
})
}
+
+ // The following cases are parser-coverage only: a BLOB column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), which lets us exercise clauses Hudi does not
support at execution
+ // time (transform partitioning, STORED AS / ROW FORMAT, interval columns).
The BLOB column type
+ // itself proves routing because the stock Spark parser rejects the BLOB
type name.
+
+ private def parse(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("Test parse CREATE TABLE with BLOB column and primitive data types") {
+ // Exercises the primitive-data-type match arms plus NOT NULL and column
COMMENT.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_prim_tbl (
+ | c_bool BOOLEAN,
+ | c_tiny TINYINT,
+ | c_small SMALLINT,
+ | c_int INT,
+ | c_big BIGINT,
+ | c_float FLOAT,
+ | c_double DOUBLE,
+ | c_date DATE,
+ | c_str STRING,
+ | c_char CHAR(5),
+ | c_varchar VARCHAR(10),
+ | c_bin BINARY,
+ | c_dec DECIMAL,
+ | c_dec1 DECIMAL(12),
+ | c_dec2 DECIMAL(12, 3),
+ | c_notnull INT NOT NULL,
+ | c_comment INT COMMENT 'a comment',
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(BooleanType)(schema("c_bool").dataType)
+ assertResult(ByteType)(schema("c_tiny").dataType)
+ assertResult(ShortType)(schema("c_small").dataType)
+ assertResult(IntegerType)(schema("c_int").dataType)
+ assertResult(LongType)(schema("c_big").dataType)
+ assertResult(FloatType)(schema("c_float").dataType)
+ assertResult(DoubleType)(schema("c_double").dataType)
+ assertResult(DateType)(schema("c_date").dataType)
+ assertResult(StringType)(schema("c_str").dataType)
+ // CHAR/VARCHAR may be preserved or replaced with STRING depending on the
Spark version.
+ assert(Seq[DataType](CharType(5),
StringType).contains(schema("c_char").dataType))
+ assert(Seq[DataType](VarcharType(10),
StringType).contains(schema("c_varchar").dataType))
+ assertResult(BinaryType)(schema("c_bin").dataType)
+ assertResult(DecimalType(10, 0))(schema("c_dec").dataType)
+ assertResult(DecimalType(12, 0))(schema("c_dec1").dataType)
+ assertResult(DecimalType(12, 3))(schema("c_dec2").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+ assert(!schema("c_notnull").nullable)
+ assertResult("a
comment")(schema("c_comment").metadata.getString("comment"))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and complex data types") {
+ // Exercises the ARRAY / MAP / STRUCT arms and BLOB-in-struct metadata
handling.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_complex_tbl (
+ | c_arr ARRAY<INT>,
+ | c_map MAP<STRING, INT>,
+ | c_struct STRUCT<a: INT, b: STRING>,
+ | c_nested_blob STRUCT<x: BLOB>,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(ArrayType(IntegerType))(schema("c_arr").dataType)
+ assertResult(MapType(StringType, IntegerType))(schema("c_map").dataType)
+ val inner = schema("c_struct").dataType.asInstanceOf[StructType]
+ assertResult(IntegerType)(inner("a").dataType)
+ assertResult(StringType)(inner("b").dataType)
+ // A BLOB nested inside a struct still carries the BLOB type descriptor.
+ val nested = schema("c_nested_blob").dataType.asInstanceOf[StructType]("x")
+ assertResult(BlobType())(nested.dataType)
+ assertResult(HoodieSchemaType.BLOB.name())(
+ nested.metadata.getString(HoodieSchema.TYPE_METADATA_FIELD))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and interval data types") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_ivl_tbl (
+ | i_year INTERVAL YEAR,
+ | i_ym INTERVAL YEAR TO MONTH,
+ | i_day INTERVAL DAY,
+ | i_ds INTERVAL DAY TO SECOND,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+
assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR))(schema("i_year").dataType)
+ assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH))(
+ schema("i_ym").dataType)
+
assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY))(schema("i_day").dataType)
+ assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY,
DayTimeIntervalType.SECOND))(
+ schema("i_ds").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+
+ // Endpoints where the end field does not follow the start are rejected by
both interval
+ // data-type visitors. The grammar only allows YEAR/MONTH -> MONTH and
DAY/HOUR/MINUTE/SECOND
+ // -> HOUR/MINUTE/SECOND, so these stay grammatical yet still hit the
builder's end <= start guard.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_ym (id BIGINT, bad INTERVAL MONTH TO MONTH, data
BLOB) USING hudi")(
+ "are not supported")
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_dt (id BIGINT, bad INTERVAL SECOND TO HOUR, data
BLOB) USING hudi")(
+ "are not supported")
+
+ // An unknown primitive type name is rejected.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_type (id BIGINT, weird sometype, data BLOB) USING
hudi")(
+ "is not supported")
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and partition transforms") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_tf_tbl (
+ | id BIGINT,
+ | ts DATE,
+ | region STRING,
+ | data BLOB
+ |) USING hudi
+ |PARTITIONED BY (region, years(ts), months(ts), days(ts), hours(ts),
myfunc(id))
+ """.stripMargin)
+ assertResult(BlobType())(plan.tableSchema("data").dataType)
+ assertResult(Seq(Seq("region")))(transformFieldRefs(transformByName(plan,
"identity")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"years")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"months")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"days")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"hours")))
+ // an arbitrary function transform falls through to the generic
apply-transform arm
+ assertResult(Seq(Seq("id")))(transformFieldRefs(transformByName(plan,
"myfunc")))
+
+ // bucket(numBuckets, col) with int, long and short number-of-buckets
literals exercises the
+ // three numeric arms of the bucket handling.
+ Seq("4", "4L", "4S").foreach { numLiteral =>
+ val bp = parse(
+ s"CREATE TABLE blob_bkt_tbl (id BIGINT, data BLOB) USING hudi " +
+ s"PARTITIONED BY (bucket($numLiteral, id))")
+ val bkt = transformByName(bp, "bucket")
+ assertResult("4")(firstLiteralArg(bkt).value.toString)
+ assertResult(Seq(Seq("id")))(transformFieldRefs(bkt))
+ }
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and typed transform-argument
literals") {
+ // Constant transform arguments exercise the literal visitors: string,
integer, big-integer and
+ // exponent numerics (the private numeric-literal helper), the typed date
constructor, and both
+ // interval forms (multi-unit and unit-to-unit). A typed timestamp
constructor is not used
+ // because the Spark 4.x extended parser rejects a bare TIMESTAMP token.
Note: a bare
+ // true/false/null in this position is parsed as a column reference
(qualifiedName takes
+ // precedence over a constant in the grammar under the default non-ANSI
config), so the boolean
+ // and null literal visitors are not reachable from a CREATE TABLE
statement.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_lit_tbl (
+ | id BIGINT,
+ | data BLOB
+ |) USING hudi
+ |PARTITIONED BY (
+ | str_t('x', id),
+ | int_t(7, id),
+ | long_t(9000000000L, id),
+ | exp_t(1E3, id),
+ | date_t(DATE '2020-01-01', id),
+ | mu_ivl_t(INTERVAL '1' DAY, id),
+ | uu_ivl_t(INTERVAL '1-2' YEAR TO MONTH, id)
+ |)
+ """.stripMargin)
+ assertResult(StringType)(firstLiteralArg(transformByName(plan,
"str_t")).dataType)
+ assertResult("x")(firstLiteralArg(transformByName(plan,
"str_t")).value.toString)
+ assertResult(IntegerType)(firstLiteralArg(transformByName(plan,
"int_t")).dataType)
+ assertResult(LongType)(firstLiteralArg(transformByName(plan,
"long_t")).dataType)
+ assertResult(DoubleType)(firstLiteralArg(transformByName(plan,
"exp_t")).dataType)
+ assertResult(DateType)(firstLiteralArg(transformByName(plan,
"date_t")).dataType)
+ assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY,
DayTimeIntervalType.DAY))(
+ firstLiteralArg(transformByName(plan, "mu_ivl_t")).dataType)
+ assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH))(
+ firstLiteralArg(transformByName(plan, "uu_ivl_t")).dataType)
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and invalid partition
transforms") {
+ // Non-numeric number of buckets.
+ checkExceptionContain(
+ "CREATE TABLE blob_e1 (id BIGINT, data BLOB) USING hudi PARTITIONED BY
(bucket('x', id))")(
+ "Invalid number of buckets")
Review Comment:
These negatives pass for the wrong reason on Spark 3.4+. There, `new
ParseException(String, ctx)` treats the String as an error **class**, so this
case actually throws `SparkException: [INTERNAL_ERROR] Cannot find main error
class 'Invalid number of buckets: 'x''` (verified at runtime on 3.5.5; only the
spark3.3 profile still throws a real `ParseException` with the message).
`checkExceptionContain` substring-matches any `Throwable`, so the assertion
passes against the broken output and cements it. This affects ~10 throw-sites
per builder (3.5 builder lines 597, 608, 623, 918, 954, ...), including a
`$nonRef.describe` interpolation bug that emits `for transform bucket:
5.describe`.
Action: file a follow-up issue to switch those sites to
`ParserUtils.operationNotAllowed(...)` or the `(command, message, start, stop)`
ctor and add the missing `${...}` braces; in this PR, strengthen at least one
of these negatives to also assert the exception type (e.g.
`intercept[ParseException]`) with a TODO referencing the issue, so the fix is
observable.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala:
##########
@@ -282,4 +284,256 @@ class TestBlobDataType extends HoodieSparkSqlTestBase {
"Expected at least one .clean instant on the timeline after
compaction")
})
}
+
+ // The following cases are parser-coverage only: a BLOB column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), which lets us exercise clauses Hudi does not
support at execution
+ // time (transform partitioning, STORED AS / ROW FORMAT, interval columns).
The BLOB column type
+ // itself proves routing because the stock Spark parser rejects the BLOB
type name.
+
+ private def parse(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("Test parse CREATE TABLE with BLOB column and primitive data types") {
+ // Exercises the primitive-data-type match arms plus NOT NULL and column
COMMENT.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_prim_tbl (
+ | c_bool BOOLEAN,
+ | c_tiny TINYINT,
+ | c_small SMALLINT,
+ | c_int INT,
+ | c_big BIGINT,
+ | c_float FLOAT,
+ | c_double DOUBLE,
+ | c_date DATE,
+ | c_str STRING,
+ | c_char CHAR(5),
+ | c_varchar VARCHAR(10),
+ | c_bin BINARY,
+ | c_dec DECIMAL,
+ | c_dec1 DECIMAL(12),
+ | c_dec2 DECIMAL(12, 3),
+ | c_notnull INT NOT NULL,
+ | c_comment INT COMMENT 'a comment',
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(BooleanType)(schema("c_bool").dataType)
+ assertResult(ByteType)(schema("c_tiny").dataType)
+ assertResult(ShortType)(schema("c_small").dataType)
+ assertResult(IntegerType)(schema("c_int").dataType)
+ assertResult(LongType)(schema("c_big").dataType)
+ assertResult(FloatType)(schema("c_float").dataType)
+ assertResult(DoubleType)(schema("c_double").dataType)
+ assertResult(DateType)(schema("c_date").dataType)
+ assertResult(StringType)(schema("c_str").dataType)
+ // CHAR/VARCHAR may be preserved or replaced with STRING depending on the
Spark version.
+ assert(Seq[DataType](CharType(5),
StringType).contains(schema("c_char").dataType))
+ assert(Seq[DataType](VarcharType(10),
StringType).contains(schema("c_varchar").dataType))
+ assertResult(BinaryType)(schema("c_bin").dataType)
+ assertResult(DecimalType(10, 0))(schema("c_dec").dataType)
+ assertResult(DecimalType(12, 0))(schema("c_dec1").dataType)
+ assertResult(DecimalType(12, 3))(schema("c_dec2").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+ assert(!schema("c_notnull").nullable)
+ assertResult("a
comment")(schema("c_comment").metadata.getString("comment"))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and complex data types") {
+ // Exercises the ARRAY / MAP / STRUCT arms and BLOB-in-struct metadata
handling.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_complex_tbl (
+ | c_arr ARRAY<INT>,
+ | c_map MAP<STRING, INT>,
+ | c_struct STRUCT<a: INT, b: STRING>,
+ | c_nested_blob STRUCT<x: BLOB>,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(ArrayType(IntegerType))(schema("c_arr").dataType)
+ assertResult(MapType(StringType, IntegerType))(schema("c_map").dataType)
+ val inner = schema("c_struct").dataType.asInstanceOf[StructType]
+ assertResult(IntegerType)(inner("a").dataType)
+ assertResult(StringType)(inner("b").dataType)
+ // A BLOB nested inside a struct still carries the BLOB type descriptor.
+ val nested = schema("c_nested_blob").dataType.asInstanceOf[StructType]("x")
+ assertResult(BlobType())(nested.dataType)
+ assertResult(HoodieSchemaType.BLOB.name())(
+ nested.metadata.getString(HoodieSchema.TYPE_METADATA_FIELD))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and interval data types") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_ivl_tbl (
+ | i_year INTERVAL YEAR,
+ | i_ym INTERVAL YEAR TO MONTH,
+ | i_day INTERVAL DAY,
+ | i_ds INTERVAL DAY TO SECOND,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+
assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR))(schema("i_year").dataType)
+ assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH))(
+ schema("i_ym").dataType)
+
assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY))(schema("i_day").dataType)
+ assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY,
DayTimeIntervalType.SECOND))(
+ schema("i_ds").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+
+ // Endpoints where the end field does not follow the start are rejected by
both interval
+ // data-type visitors. The grammar only allows YEAR/MONTH -> MONTH and
DAY/HOUR/MINUTE/SECOND
+ // -> HOUR/MINUTE/SECOND, so these stay grammatical yet still hit the
builder's end <= start guard.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_ym (id BIGINT, bad INTERVAL MONTH TO MONTH, data
BLOB) USING hudi")(
+ "are not supported")
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_dt (id BIGINT, bad INTERVAL SECOND TO HOUR, data
BLOB) USING hudi")(
+ "are not supported")
+
+ // An unknown primitive type name is rejected.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_type (id BIGINT, weird sometype, data BLOB) USING
hudi")(
+ "is not supported")
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and partition transforms") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_tf_tbl (
+ | id BIGINT,
+ | ts DATE,
+ | region STRING,
+ | data BLOB
+ |) USING hudi
+ |PARTITIONED BY (region, years(ts), months(ts), days(ts), hours(ts),
myfunc(id))
+ """.stripMargin)
+ assertResult(BlobType())(plan.tableSchema("data").dataType)
+ assertResult(Seq(Seq("region")))(transformFieldRefs(transformByName(plan,
"identity")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"years")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"months")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"days")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"hours")))
+ // an arbitrary function transform falls through to the generic
apply-transform arm
+ assertResult(Seq(Seq("id")))(transformFieldRefs(transformByName(plan,
"myfunc")))
+
+ // bucket(numBuckets, col) with int, long and short number-of-buckets
literals exercises the
+ // three numeric arms of the bucket handling.
+ Seq("4", "4L", "4S").foreach { numLiteral =>
+ val bp = parse(
+ s"CREATE TABLE blob_bkt_tbl (id BIGINT, data BLOB) USING hudi " +
+ s"PARTITIONED BY (bucket($numLiteral, id))")
+ val bkt = transformByName(bp, "bucket")
+ assertResult("4")(firstLiteralArg(bkt).value.toString)
+ assertResult(Seq(Seq("id")))(transformFieldRefs(bkt))
+ }
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and typed transform-argument
literals") {
+ // Constant transform arguments exercise the literal visitors: string,
integer, big-integer and
+ // exponent numerics (the private numeric-literal helper), the typed date
constructor, and both
+ // interval forms (multi-unit and unit-to-unit). A typed timestamp
constructor is not used
+ // because the Spark 4.x extended parser rejects a bare TIMESTAMP token.
Note: a bare
+ // true/false/null in this position is parsed as a column reference
(qualifiedName takes
+ // precedence over a constant in the grammar under the default non-ANSI
config), so the boolean
+ // and null literal visitors are not reachable from a CREATE TABLE
statement.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_lit_tbl (
+ | id BIGINT,
+ | data BLOB
+ |) USING hudi
+ |PARTITIONED BY (
+ | str_t('x', id),
+ | int_t(7, id),
+ | long_t(9000000000L, id),
+ | exp_t(1E3, id),
+ | date_t(DATE '2020-01-01', id),
+ | mu_ivl_t(INTERVAL '1' DAY, id),
+ | uu_ivl_t(INTERVAL '1-2' YEAR TO MONTH, id)
+ |)
+ """.stripMargin)
+ assertResult(StringType)(firstLiteralArg(transformByName(plan,
"str_t")).dataType)
+ assertResult("x")(firstLiteralArg(transformByName(plan,
"str_t")).value.toString)
+ assertResult(IntegerType)(firstLiteralArg(transformByName(plan,
"int_t")).dataType)
+ assertResult(LongType)(firstLiteralArg(transformByName(plan,
"long_t")).dataType)
+ assertResult(DoubleType)(firstLiteralArg(transformByName(plan,
"exp_t")).dataType)
+ assertResult(DateType)(firstLiteralArg(transformByName(plan,
"date_t")).dataType)
+ assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY,
DayTimeIntervalType.DAY))(
+ firstLiteralArg(transformByName(plan, "mu_ivl_t")).dataType)
+ assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH))(
+ firstLiteralArg(transformByName(plan, "uu_ivl_t")).dataType)
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and invalid partition
transforms") {
+ // Non-numeric number of buckets.
+ checkExceptionContain(
+ "CREATE TABLE blob_e1 (id BIGINT, data BLOB) USING hudi PARTITIONED BY
(bucket('x', id))")(
+ "Invalid number of buckets")
+ // A non-column-reference where a column is required.
+ checkExceptionContain(
+ "CREATE TABLE blob_e2 (id BIGINT, data BLOB) USING hudi PARTITIONED BY
(bucket(4, 5))")(
+ "Expected a column reference")
+ // A single-field transform given more than one argument.
+ checkExceptionContain(
+ "CREATE TABLE blob_e3 (id BIGINT, ts DATE, data BLOB) USING hudi " +
+ "PARTITIONED BY (years(id, ts))")(
+ "Too many arguments")
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and file-format / row-format
clauses") {
+ // Generic STORED AS format.
+ assertResult(BlobType())(
+ parse("CREATE TABLE blob_ff1 (id BIGINT, data BLOB) STORED AS PARQUET")
Review Comment:
All six positive cases assert only `BlobType()`, which is identical whether
or not the file/row-format visitors produce anything -- a builder that silently
dropped the serde info would still pass. The discriminating values are already
on the plan and portable across 3.3-4.2: `blob_ff1` -> `tableSpec.serde ==
Some(SerdeInfo(storedAs = Some("PARQUET"), ...))`, `blob_ff2` ->
`formatClasses`, `blob_ff3` -> `serde`, `blob_ff4` ->
`serdeProperties("field.delim" -> ",")`.
Action: assert the relevant `plan.tableSpec.serde` field per case instead of
the column type.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala:
##########
@@ -2340,6 +2341,140 @@ class TestCreateTable extends HoodieSparkSqlTestBase {
}
}
+ // The following cases are parser-coverage only: a VECTOR column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), matching how TestIndexSyntax exercises the
index statements, which
+ // lets us cover clauses that are not supported at execution time (transform
partitioning,
+ // CLUSTERED BY, typed literal arguments). The VECTOR column type proves the
statement routed
+ // here because the stock Spark parser rejects the VECTOR type name.
+
+ private def parseCreateTable(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("test create VECTOR table with partition transforms parses (parser
coverage)") {
+ val plan = parseCreateTable(
+ s"""
+ |CREATE TABLE vec_parse_tbl (
+ | id BIGINT,
+ | ts DATE,
+ | region STRING,
+ | name STRING,
+ | embedding VECTOR(8)
+ |) USING hudi
+ |PARTITIONED BY (region, bucket(4, id), years(ts), truncate(10, name))
+ """.stripMargin)
+
+ assertEquals(ArrayType(FloatType, containsNull = false),
plan.tableSchema("embedding").dataType)
+
+ assertEquals(Seq(Seq("region")), transformFieldRefs(transformByName(plan,
"identity")))
+ val bucket = transformByName(plan, "bucket")
+ assertEquals(IntegerType, firstLiteralArg(bucket).dataType)
+ assertEquals("4", firstLiteralArg(bucket).value.toString)
+ assertEquals(Seq(Seq("id")), transformFieldRefs(bucket))
+ assertEquals(Seq(Seq("ts")), transformFieldRefs(transformByName(plan,
"years")))
+ val truncate = transformByName(plan, "truncate")
+ assertEquals(IntegerType, firstLiteralArg(truncate).dataType)
+ assertEquals("10", firstLiteralArg(truncate).value.toString)
+ assertEquals(Seq(Seq("name")), transformFieldRefs(truncate))
+ }
+
+ test("test create VECTOR table with typed transform-argument literals parses
(parser coverage)") {
+ // Each transform carries a differently-typed constant argument to
exercise the literal
+ // visitors: string, integer, long, exponent/double, the typed date
constructor, and both
+ // interval forms (multi-units and unit-to-unit). A typed timestamp
constructor is not used
+ // because the Spark 4.x extended parser rejects a bare TIMESTAMP token. A
bare true/false/null
+ // in this position is parsed as a column reference (qualifiedName takes
precedence over a
+ // constant in the grammar under the default non-ANSI config), so the
boolean and null literal
+ // visitors are not reachable from a CREATE TABLE statement.
+ val plan = parseCreateTable(
+ s"""
+ |CREATE TABLE vec_lit_tbl (
+ | id BIGINT,
+ | embedding VECTOR(4)
+ |) USING hudi
+ |PARTITIONED BY (
+ | str_t('x', id),
+ | int_t(4, id),
+ | long_t(9000000000L, id),
+ | exp_t(1E3, id),
+ | date_t(DATE '2020-01-01', id),
+ | mu_ivl_t(INTERVAL '1' DAY, id),
+ | uu_ivl_t(INTERVAL '1-2' YEAR TO MONTH, id)
+ |)
+ """.stripMargin)
+
+ assertEquals(StringType, firstLiteralArg(transformByName(plan,
"str_t")).dataType)
+ assertEquals("x", firstLiteralArg(transformByName(plan,
"str_t")).value.toString)
+ assertEquals(IntegerType, firstLiteralArg(transformByName(plan,
"int_t")).dataType)
+ assertEquals(LongType, firstLiteralArg(transformByName(plan,
"long_t")).dataType)
+ assertEquals(DoubleType, firstLiteralArg(transformByName(plan,
"exp_t")).dataType)
+ assertEquals(DateType, firstLiteralArg(transformByName(plan,
"date_t")).dataType)
+ assertEquals(
+ DayTimeIntervalType(DayTimeIntervalType.DAY, DayTimeIntervalType.DAY),
+ firstLiteralArg(transformByName(plan, "mu_ivl_t")).dataType)
+ assertEquals(
+ YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH),
+ firstLiteralArg(transformByName(plan, "uu_ivl_t")).dataType)
+ }
+
+ test("test create VECTOR table with CLUSTERED BY bucket spec parses (parser
coverage)") {
+ val plan = parseCreateTable(
+ "CREATE TABLE vec_bucket_tbl (id BIGINT, embedding VECTOR(4)) USING hudi
" +
+ "CLUSTERED BY (id) INTO 4 BUCKETS")
+ val bucket = transformByName(plan, "bucket")
+ assertEquals("4", firstLiteralArg(bucket).value.toString)
+ assertEquals(Seq(Seq("id")), transformFieldRefs(bucket))
+
+ // SORTED BY ... ASC is accepted by the bucket-spec visitor.
+ val sortedPlan = parseCreateTable(
+ "CREATE TABLE vec_sbucket_tbl (id BIGINT, ts BIGINT, embedding
VECTOR(4)) USING hudi " +
+ "CLUSTERED BY (id, ts) SORTED BY (id ASC) INTO 8 BUCKETS")
+
assertTrue(sortedPlan.partitioning.exists(_.name.toLowerCase.contains("bucket")))
Review Comment:
This cannot fail for the case it claims to test: plain
`BucketTransform.name` is `"bucket"` and `SortedBucketTransform.name` is
`"sorted_bucket"` (spark-catalyst bytecode, consistent 3.3-4.1), so
`contains("bucket")` passes whether or not SORTED BY was honored.
```suggestion
assertEquals("sorted_bucket", sortedPlan.partitioning.head.name)
```
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/feature/index/TestIndexSyntax.scala:
##########
@@ -95,6 +95,26 @@ class TestIndexSyntax extends HoodieSparkSqlTestBase {
resolvedLogicalPlan = analyzer.execute(logicalPlan)
assertTableIdentifier(resolvedLogicalPlan.asInstanceOf[RefreshIndexCommand].table,
databaseName, tableName)
assertResult("idx_name")(resolvedLogicalPlan.asInstanceOf[RefreshIndexCommand].indexName)
+
+ // CREATE INDEX without a USING clause: the index type defaults to
empty
+ logicalPlan = sqlParser.parsePlan(s"create index idx_default on
$tableName (name)")
+ resolvedLogicalPlan = analyzer.execute(logicalPlan)
+
assertTableIdentifier(resolvedLogicalPlan.asInstanceOf[CreateIndexCommand].table,
databaseName, tableName)
+
assertResult("idx_default")(resolvedLogicalPlan.asInstanceOf[CreateIndexCommand].indexName)
+
assertResult("")(resolvedLogicalPlan.asInstanceOf[CreateIndexCommand].indexType)
+
assertResult(false)(resolvedLogicalPlan.asInstanceOf[CreateIndexCommand].ignoreIfExists)
Review Comment:
This duplicates the identical assertion at line 77.
```suggestion
```
Also, for the commit message framing: the no-USING create and no-IF-EXISTS
drop *branches* already execute (lines 158-159 in this file, plus ~40 `create
index ... (col)` calls across the SI suites), and `SHOW INDEXES IN` adds no
builder branch (`visitShowIndexes` ignores FROM vs IN). The new plan-field
pinning (`indexType == ""`, `ignoreIfNotExists == false`) is the actual delta
and worth keeping.
##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala:
##########
@@ -282,4 +284,256 @@ class TestBlobDataType extends HoodieSparkSqlTestBase {
"Expected at least one .clean instant on the timeline after
compaction")
})
}
+
+ // The following cases are parser-coverage only: a BLOB column routes the
whole CREATE TABLE
+ // through the extended AST builder, so its clause visitors run. parsePlan
is purely syntactic
+ // (no catalog, no execution), which lets us exercise clauses Hudi does not
support at execution
+ // time (transform partitioning, STORED AS / ROW FORMAT, interval columns).
The BLOB column type
+ // itself proves routing because the stock Spark parser rejects the BLOB
type name.
+
+ private def parse(sql: String): CreateTable =
+ spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
+
+ test("Test parse CREATE TABLE with BLOB column and primitive data types") {
+ // Exercises the primitive-data-type match arms plus NOT NULL and column
COMMENT.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_prim_tbl (
+ | c_bool BOOLEAN,
+ | c_tiny TINYINT,
+ | c_small SMALLINT,
+ | c_int INT,
+ | c_big BIGINT,
+ | c_float FLOAT,
+ | c_double DOUBLE,
+ | c_date DATE,
+ | c_str STRING,
+ | c_char CHAR(5),
+ | c_varchar VARCHAR(10),
+ | c_bin BINARY,
+ | c_dec DECIMAL,
+ | c_dec1 DECIMAL(12),
+ | c_dec2 DECIMAL(12, 3),
+ | c_notnull INT NOT NULL,
+ | c_comment INT COMMENT 'a comment',
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(BooleanType)(schema("c_bool").dataType)
+ assertResult(ByteType)(schema("c_tiny").dataType)
+ assertResult(ShortType)(schema("c_small").dataType)
+ assertResult(IntegerType)(schema("c_int").dataType)
+ assertResult(LongType)(schema("c_big").dataType)
+ assertResult(FloatType)(schema("c_float").dataType)
+ assertResult(DoubleType)(schema("c_double").dataType)
+ assertResult(DateType)(schema("c_date").dataType)
+ assertResult(StringType)(schema("c_str").dataType)
+ // CHAR/VARCHAR may be preserved or replaced with STRING depending on the
Spark version.
+ assert(Seq[DataType](CharType(5),
StringType).contains(schema("c_char").dataType))
+ assert(Seq[DataType](VarcharType(10),
StringType).contains(schema("c_varchar").dataType))
+ assertResult(BinaryType)(schema("c_bin").dataType)
+ assertResult(DecimalType(10, 0))(schema("c_dec").dataType)
+ assertResult(DecimalType(12, 0))(schema("c_dec1").dataType)
+ assertResult(DecimalType(12, 3))(schema("c_dec2").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+ assert(!schema("c_notnull").nullable)
+ assertResult("a
comment")(schema("c_comment").metadata.getString("comment"))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and complex data types") {
+ // Exercises the ARRAY / MAP / STRUCT arms and BLOB-in-struct metadata
handling.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_complex_tbl (
+ | c_arr ARRAY<INT>,
+ | c_map MAP<STRING, INT>,
+ | c_struct STRUCT<a: INT, b: STRING>,
+ | c_nested_blob STRUCT<x: BLOB>,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+ assertResult(ArrayType(IntegerType))(schema("c_arr").dataType)
+ assertResult(MapType(StringType, IntegerType))(schema("c_map").dataType)
+ val inner = schema("c_struct").dataType.asInstanceOf[StructType]
+ assertResult(IntegerType)(inner("a").dataType)
+ assertResult(StringType)(inner("b").dataType)
+ // A BLOB nested inside a struct still carries the BLOB type descriptor.
+ val nested = schema("c_nested_blob").dataType.asInstanceOf[StructType]("x")
+ assertResult(BlobType())(nested.dataType)
+ assertResult(HoodieSchemaType.BLOB.name())(
+ nested.metadata.getString(HoodieSchema.TYPE_METADATA_FIELD))
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and interval data types") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_ivl_tbl (
+ | i_year INTERVAL YEAR,
+ | i_ym INTERVAL YEAR TO MONTH,
+ | i_day INTERVAL DAY,
+ | i_ds INTERVAL DAY TO SECOND,
+ | data BLOB
+ |) USING hudi
+ """.stripMargin)
+ val schema = plan.tableSchema
+
assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR))(schema("i_year").dataType)
+ assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH))(
+ schema("i_ym").dataType)
+
assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY))(schema("i_day").dataType)
+ assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY,
DayTimeIntervalType.SECOND))(
+ schema("i_ds").dataType)
+ assertResult(BlobType())(schema("data").dataType)
+
+ // Endpoints where the end field does not follow the start are rejected by
both interval
+ // data-type visitors. The grammar only allows YEAR/MONTH -> MONTH and
DAY/HOUR/MINUTE/SECOND
+ // -> HOUR/MINUTE/SECOND, so these stay grammatical yet still hit the
builder's end <= start guard.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_ym (id BIGINT, bad INTERVAL MONTH TO MONTH, data
BLOB) USING hudi")(
+ "are not supported")
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_dt (id BIGINT, bad INTERVAL SECOND TO HOUR, data
BLOB) USING hudi")(
+ "are not supported")
+
+ // An unknown primitive type name is rejected.
+ checkExceptionContain(
+ "CREATE TABLE blob_bad_type (id BIGINT, weird sometype, data BLOB) USING
hudi")(
+ "is not supported")
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and partition transforms") {
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_tf_tbl (
+ | id BIGINT,
+ | ts DATE,
+ | region STRING,
+ | data BLOB
+ |) USING hudi
+ |PARTITIONED BY (region, years(ts), months(ts), days(ts), hours(ts),
myfunc(id))
+ """.stripMargin)
+ assertResult(BlobType())(plan.tableSchema("data").dataType)
+ assertResult(Seq(Seq("region")))(transformFieldRefs(transformByName(plan,
"identity")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"years")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"months")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"days")))
+ assertResult(Seq(Seq("ts")))(transformFieldRefs(transformByName(plan,
"hours")))
+ // an arbitrary function transform falls through to the generic
apply-transform arm
+ assertResult(Seq(Seq("id")))(transformFieldRefs(transformByName(plan,
"myfunc")))
+
+ // bucket(numBuckets, col) with int, long and short number-of-buckets
literals exercises the
+ // three numeric arms of the bucket handling.
+ Seq("4", "4L", "4S").foreach { numLiteral =>
+ val bp = parse(
+ s"CREATE TABLE blob_bkt_tbl (id BIGINT, data BLOB) USING hudi " +
+ s"PARTITIONED BY (bucket($numLiteral, id))")
+ val bkt = transformByName(bp, "bucket")
+ assertResult("4")(firstLiteralArg(bkt).value.toString)
+ assertResult(Seq(Seq("id")))(transformFieldRefs(bkt))
+ }
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and typed transform-argument
literals") {
+ // Constant transform arguments exercise the literal visitors: string,
integer, big-integer and
+ // exponent numerics (the private numeric-literal helper), the typed date
constructor, and both
+ // interval forms (multi-unit and unit-to-unit). A typed timestamp
constructor is not used
+ // because the Spark 4.x extended parser rejects a bare TIMESTAMP token.
Note: a bare
+ // true/false/null in this position is parsed as a column reference
(qualifiedName takes
+ // precedence over a constant in the grammar under the default non-ANSI
config), so the boolean
+ // and null literal visitors are not reachable from a CREATE TABLE
statement.
+ val plan = parse(
+ s"""
+ |CREATE TABLE blob_lit_tbl (
+ | id BIGINT,
+ | data BLOB
+ |) USING hudi
+ |PARTITIONED BY (
+ | str_t('x', id),
+ | int_t(7, id),
+ | long_t(9000000000L, id),
+ | exp_t(1E3, id),
+ | date_t(DATE '2020-01-01', id),
+ | mu_ivl_t(INTERVAL '1' DAY, id),
+ | uu_ivl_t(INTERVAL '1-2' YEAR TO MONTH, id)
+ |)
+ """.stripMargin)
+ assertResult(StringType)(firstLiteralArg(transformByName(plan,
"str_t")).dataType)
+ assertResult("x")(firstLiteralArg(transformByName(plan,
"str_t")).value.toString)
+ assertResult(IntegerType)(firstLiteralArg(transformByName(plan,
"int_t")).dataType)
+ assertResult(LongType)(firstLiteralArg(transformByName(plan,
"long_t")).dataType)
+ assertResult(DoubleType)(firstLiteralArg(transformByName(plan,
"exp_t")).dataType)
+ assertResult(DateType)(firstLiteralArg(transformByName(plan,
"date_t")).dataType)
+ assertResult(DayTimeIntervalType(DayTimeIntervalType.DAY,
DayTimeIntervalType.DAY))(
+ firstLiteralArg(transformByName(plan, "mu_ivl_t")).dataType)
+ assertResult(YearMonthIntervalType(YearMonthIntervalType.YEAR,
YearMonthIntervalType.MONTH))(
+ firstLiteralArg(transformByName(plan, "uu_ivl_t")).dataType)
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and invalid partition
transforms") {
+ // Non-numeric number of buckets.
+ checkExceptionContain(
+ "CREATE TABLE blob_e1 (id BIGINT, data BLOB) USING hudi PARTITIONED BY
(bucket('x', id))")(
+ "Invalid number of buckets")
+ // A non-column-reference where a column is required.
+ checkExceptionContain(
+ "CREATE TABLE blob_e2 (id BIGINT, data BLOB) USING hudi PARTITIONED BY
(bucket(4, 5))")(
+ "Expected a column reference")
+ // A single-field transform given more than one argument.
+ checkExceptionContain(
+ "CREATE TABLE blob_e3 (id BIGINT, ts DATE, data BLOB) USING hudi " +
+ "PARTITIONED BY (years(id, ts))")(
+ "Too many arguments")
+ }
+
+ test("Test parse CREATE TABLE with BLOB column and file-format / row-format
clauses") {
+ // Generic STORED AS format.
+ assertResult(BlobType())(
+ parse("CREATE TABLE blob_ff1 (id BIGINT, data BLOB) STORED AS PARQUET")
+ .tableSchema("data").dataType)
+ // STORED AS INPUTFORMAT ... OUTPUTFORMAT ... (the table-file-format arm).
+ assertResult(BlobType())(
+ parse("CREATE TABLE blob_ff2 (id BIGINT, data BLOB) " +
+ "STORED AS INPUTFORMAT 'com.example.InFmt' OUTPUTFORMAT
'com.example.OutFmt'")
+ .tableSchema("data").dataType)
+ // ROW FORMAT SERDE on its own.
+ assertResult(BlobType())(
+ parse("CREATE TABLE blob_ff3 (id BIGINT, data BLOB) " +
+ "ROW FORMAT SERDE
'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe'")
+ .tableSchema("data").dataType)
+ // ROW FORMAT DELIMITED on its own.
+ assertResult(BlobType())(
+ parse("CREATE TABLE blob_ff4 (id BIGINT, data BLOB) " +
+ "ROW FORMAT DELIMITED FIELDS TERMINATED BY ','")
+ .tableSchema("data").dataType)
+ // Compatible ROW FORMAT SERDE + STORED AS SEQUENCEFILE.
+ assertResult(BlobType())(
+ parse("CREATE TABLE blob_ff5 (id BIGINT, data BLOB) " +
+ "ROW FORMAT SERDE 'com.example.Serde' STORED AS SEQUENCEFILE")
+ .tableSchema("data").dataType)
+ // Compatible ROW FORMAT DELIMITED + STORED AS TEXTFILE.
+ assertResult(BlobType())(
+ parse("CREATE TABLE blob_ff6 (id BIGINT, data BLOB) " +
+ "ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS TEXTFILE")
+ .tableSchema("data").dataType)
+
+ // ROW FORMAT DELIMITED with a non-text file format is rejected.
+ checkExceptionContain(
+ "CREATE TABLE blob_ferr1 (id BIGINT, data BLOB) " +
+ "ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' STORED AS PARQUET")(
+ "only compatible with 'textfile'")
+ // ROW FORMAT SERDE with a format that also specifies a serde is rejected.
+ checkExceptionContain(
+ "CREATE TABLE blob_ferr2 (id BIGINT, data BLOB) " +
+ "ROW FORMAT SERDE 'com.example.Serde' STORED AS PARQUET")(
+ "incompatible with format")
+ // STORED BY (a storage handler) is not allowed.
+ checkExceptionContain(
+ "CREATE TABLE blob_ferr3 (id BIGINT, data BLOB) STORED BY
'com.example.Handler'")(
+ "STORED BY")
Review Comment:
This negative is vacuous: `ParseException.getMessage` appends the offending
statement under `== SQL ==`, so the expected substring `STORED BY` matches no
matter why the parse failed -- even if the statement were routed to the stock
Spark parser. Pin the actual error:
```suggestion
"Operation not allowed: STORED BY")
```
--
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]