voonhous commented on code in PR #19648:
URL: https://github.com/apache/hudi/pull/19648#discussion_r3813353926


##########
hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/keygen/PartitionPathFormatterBase.java:
##########
@@ -75,15 +75,11 @@ public final S combine(List<String> partitionPathFields, 
Object... partitionPath
 
       if (useHiveStylePartitioning) {
         sb.appendJava(partitionPathFields.get(i))
-            .appendJava("=")
-            .append(partitionPathPartStr);
-      } else if (slashSeparatedDatePartitioning) {
-        String res = ((String) partitionPathPartStr).replace('-', '/');
-        sb.append(((S) res));
-      } else {
-        sb.append(partitionPathPartStr);
+            .appendJava("=");
       }
 
+      sb.append(partitionPathPartStr);

Review Comment:
   The multi-field branch this rewrites is not reached by any write path: 
`getPartitionPath(Row)` has no production caller whose output lands on disk, 
and the `InternalRow` side threw on master. Its one production consumer is 
query-side pruning: `SparkHoodieTableFileIndex.composeRelativePartitionPath` 
(`:471-489`) builds a `StringPartitionPathFormatter` from table config and 
`combine`s only the bound *prefix* of partition columns (`:404`).
   
   For a `(datestr, city)` slash table written by the Avro path 
(`2026-01-05/san-francisco`), `where datestr = '2026-01-05'` alone composes the 
single-part `2026/01/05`, `exists()` fails at `:444`, and the query silently 
returns zero rows. Same on master; this PR only fixes the all-columns-bound 
case (master composed `2026/01/05/san/francisco`). 
`ShowHoodieTablePartitionsCommand.scala:60-61` already `checkState`s "Only one 
partition field is allowed for SlashEncodedPartitioning".
   
   Ask: (1) reject `slash.separated.date.partitioning=true` with more than one 
partition field at table creation and in writer config validation, next to the 
existing hive-style check (or file a follow-up issue and link it here); (2) 
reword the PR "Behavior change" section -- the multi-field change lands in 
file-index prefix pruning, not in any write path, and no existing table layout 
changes.



##########
hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/keygen/PartitionPathFormatterBase.java:
##########
@@ -62,11 +62,11 @@ public final S combine(List<String> partitionPathFields, 
Object... partitionPath
     // Avoid creating [[StringBuilder]] in case there's just one 
partition-path part,
     // and Hive-style of partitioning is not required
     if (!useHiveStylePartitioning && partitionPathParts.length == 1) {
-      if (slashSeparatedDatePartitioning) {
-        return ((S) ((String) toString(partitionPathParts[0])).replace('-', 
'/'));
-      } else {
-        return tryEncode(handleEmpty(toString(partitionPathParts[0])));
-      }
+      S partitionPathPart = 
tryEncode(handleEmpty(toString(partitionPathParts[0])));
+      // NOTE: Slash-separated date partitioning only kicks in for a table 
partitioned by a single

Review Comment:
   This NOTE is wrong for `CustomKeyGenerator`: it builds one single-field 
sub-keygen per partition field (`CustomKeyGenerator.java:82-101`), so each 
field takes this fast path and `date:simple,city:simple` yields 
`2026/01/05/san/francisco` on Avro, Row and InternalRow alike (Avro does the 
same via `KeyGenUtils#getPartitionPath`, which slashes unconditionally). This 
PR is also what un-breaks that InternalRow path (same CCE), and nothing tests 
it.
   
   Ask: scope the NOTE to `SimpleKeyGenerator`/`ComplexKeyGenerator` and cite 
`KeyGenUtils#getPartitionPath` (the method governing the single-field case) 
alongside `getRecordPartitionPath`; add Row + InternalRow asserts to the 
existing `TestCustomKeyGenerator.testSlashSeparatedDatePartitioning`, which 
already builds the record.



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestSlashSeparatedPartitionValue.scala:
##########
@@ -90,6 +90,61 @@ class TestSlashSeparatedPartitionValue extends 
HoodieSparkSqlTestBase {
     }
   }
 
+  test("Test slash separated date partitions written through the row writer") {
+    withSQLConf("hoodie.sql.bulk.insert.enable" -> "true", 
"hoodie.sql.insert.mode" -> "non-strict",
+      "hoodie.datasource.write.row.writer.enable" -> "true") {
+      withTempDir { tmp =>
+        val targetTable = generateTableName
+        val tablePath = s"${tmp.getCanonicalPath}/$targetTable"
+
+        spark.sql(
+          s"""
+             |create table $targetTable (
+             |  `id` string,
+             |  `name` string,
+             |  `ts` bigint,
+             |  `datestr` STRING

Review Comment:
   Every slash test in the repo partitions on a STRING column pre-formatted 
`yyyy-MM-dd`; none uses the DATE/TIMESTAMP type the feature is documented for. 
DATE agrees on all three paths (`LocalDate.toString` via 
`BuiltinKeyGenerator.convertToLogicalDataType` and 
`HoodieAvroUtils.convertValueForAvroLogicalTypes`) but is unpinned. TIMESTAMP 
diverges: the slash arm at `SqlKeyGenerator.scala:164-166` (from #17787) sits 
ahead of the `TimestampType` normalization (cf. #12621) and shadows it, so the 
Avro path (default `consistent.logical.timestamp=false`) writes raw micros 
`1767607200000000` while the row writer now writes `2026/01/05 18:00:00.0` (it 
threw before this PR).
   
   Ask: add a DATE column case to this suite (insert + bulk_insert, same 
directory, `datestr` reads back as `date'2026-01-05'`). For TIMESTAMP either 
move the slash arm inside `case _ =>` of the dataType match or reject 
TimestampType partition columns under slash -- a linked follow-up issue is fine.



##########
hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/keygen/TestSimpleKeyGenerator.java:
##########
@@ -213,4 +213,34 @@ void 
testSlashSeparatedDatePartitioningWithAlreadyFormattedInput() {
     Assertions.assertEquals("key1", key.getRecordKey());
     Assertions.assertEquals("2026/01/01", key.getPartitionPath());
   }
+
+  @Test
+  void testSlashSeparatedDatePartitioningOnRowWritingPaths() {
+    TypedProperties properties = getPropsWithSlashSeparatedDatePartitioning();
+    // NOTE: "ts_ms" is the string-typed field of the example schema, 
"timestamp" is a long
+    properties.put(KeyGeneratorOptions.PARTITIONPATH_FIELD_NAME.key(), 
"ts_ms");
+    SimpleKeyGenerator keyGenerator = new SimpleKeyGenerator(properties);
+
+    GenericRecord avroRecord = getRecord();
+    Assertions.assertEquals("2020/03/21", 
keyGenerator.getPartitionPath(avroRecord));
+
+    Row row = KeyGeneratorTestUtilities.getRow(avroRecord);
+    Assertions.assertEquals("2020/03/21", keyGenerator.getPartitionPath(row));
+
+    InternalRow internalRow = KeyGeneratorTestUtilities.getInternalRow(row);
+    Assertions.assertEquals(UTF8String.fromString("2020/03/21"),
+        keyGenerator.getPartitionPath(internalRow, row.schema()));
+  }
+
+  @Test
+  void testSlashSeparatedDatePartitioningWithNullValue() {
+    TypedProperties properties = getPropsWithSlashSeparatedDatePartitioning();
+    properties.put(KeyGeneratorOptions.PARTITIONPATH_FIELD_NAME.key(), 
"nested_col.prop1");
+    SimpleKeyGenerator keyGenerator = new SimpleKeyGenerator(properties);
+
+    GenericRecord avroRecord = getRecord(getNestedColRecord(null, 10L));
+
+    Row row = KeyGeneratorTestUtilities.getRow(avroRecord);
+    Assertions.assertEquals(HUDI_DEFAULT_PARTITION_PATH, 
keyGenerator.getPartitionPath(row));

Review Comment:
   This only asserts the Row path; the InternalRow + null case (the row-writer 
NPE the PR describes) is asserted at no keygen level. The two extra lines below 
go red on master with the same NPE.
   ```suggestion
       Assertions.assertEquals(HUDI_DEFAULT_PARTITION_PATH, 
keyGenerator.getPartitionPath(row));
   
       InternalRow internalRow = KeyGeneratorTestUtilities.getInternalRow(row);
       
Assertions.assertEquals(UTF8String.fromString(HUDI_DEFAULT_PARTITION_PATH),
           keyGenerator.getPartitionPath(internalRow, row.schema()));
   ```



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestSlashSeparatedPartitionValue.scala:
##########
@@ -90,6 +90,61 @@ class TestSlashSeparatedPartitionValue extends 
HoodieSparkSqlTestBase {
     }
   }
 
+  test("Test slash separated date partitions written through the row writer") {
+    withSQLConf("hoodie.sql.bulk.insert.enable" -> "true", 
"hoodie.sql.insert.mode" -> "non-strict",
+      "hoodie.datasource.write.row.writer.enable" -> "true") {
+      withTempDir { tmp =>
+        val targetTable = generateTableName
+        val tablePath = s"${tmp.getCanonicalPath}/$targetTable"
+
+        spark.sql(
+          s"""
+             |create table $targetTable (
+             |  `id` string,
+             |  `name` string,
+             |  `ts` bigint,
+             |  `datestr` STRING
+             |) using hudi
+             | tblproperties (
+             |  'primaryKey' = 'id',
+             |  'type' = 'COW',
+             |  'preCombineField'='ts',
+             |  
'hoodie.datasource.write.slash.separated.date.partitioning'='true'
+             | )
+             | partitioned by (`datestr`)
+             | location '$tablePath'
+          """.stripMargin)
+
+        // NOTE: The row writer derives the partition path off of an 
[[InternalRow]], which used to
+        //       blow up with a [[ClassCastException]]; a null partition value 
used to NPE
+        spark.sql(
+          s"""
+             | insert into $targetTable values
+             | (1, 'a1', 1000, "2026-01-05"),
+             | (2, 'a2', 2000, "2026-01-06"),
+             | (3, 'a3', 3000, null)
+          """.stripMargin)
+
+        checkAnswer(s"select id, name, ts, _hoodie_partition_path from 
$targetTable order by id")(
+          Seq("1", "a1", 1000, "2026/01/05"),
+          Seq("2", "a2", 2000, "2026/01/06"),
+          Seq("3", "a3", 3000, "__HIVE_DEFAULT_PARTITION__")

Review Comment:
   Siblings at `:62` and `:185` project `datestr`; this one does not, so the 
read-back through `HoodieSparkUtils.doParsePartitionColumnValues` for 
row-writer data and the null row is unasserted.
   ```suggestion
           checkAnswer(s"select id, name, ts, _hoodie_partition_path, datestr 
from $targetTable order by id")(
             Seq("1", "a1", 1000, "2026/01/05", "2026-01-05"),
             Seq("2", "a2", 2000, "2026/01/06", "2026-01-06"),
             Seq("3", "a3", 3000, "__HIVE_DEFAULT_PARTITION__", null)
   ```



##########
hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/keygen/TestPartitionPathFormatter.java:
##########
@@ -0,0 +1,115 @@
+/*
+ * 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.hudi.keygen;
+
+import org.apache.spark.unsafe.types.UTF8String;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static 
org.apache.hudi.common.util.PartitionPathEncodeUtils.DEFAULT_PARTITION_PATH;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests the partition-path formatters backing the key generators, making sure 
that both the
+ * {@link String} (Avro/{@link org.apache.spark.sql.Row} write path) and the 
{@link UTF8String}
+ * (row-writer/{@link org.apache.spark.sql.catalyst.InternalRow} write path) 
flavors produce
+ * identical partition paths.
+ */
+class TestPartitionPathFormatter {
+
+  private static final List<String> SINGLE_FIELD = 
Collections.singletonList("date_col");
+  private static final List<String> TWO_FIELDS = Arrays.asList("date_col", 
"city");
+
+  private String combine(boolean useRowWriterPath,
+                         boolean hiveStylePartitioning,
+                         boolean encode,
+                         boolean slashSeparatedDatePartitioning,
+                         List<String> fields,
+                         Object... parts) {
+    if (useRowWriterPath) {
+      return new UTF8StringPartitionPathFormatter(
+          UTF8StringPartitionPathFormatter.UTF8StringBuilder::new, 
hiveStylePartitioning, encode,
+          slashSeparatedDatePartitioning).combine(fields, parts).toString();
+    }
+    return new StringPartitionPathFormatter(
+        StringPartitionPathFormatter.JavaStringBuilder::new, 
hiveStylePartitioning, encode,
+        slashSeparatedDatePartitioning).combine(fields, parts);
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void testSlashSeparatedDatePartitioningSingleField(boolean useRowWriterPath) 
{

Review Comment:
   Delete: the only assert that fails on master is the UTF8 run of the first 
line (CCE), which `...EncodesValues`, `...OnlyAppliesToSingleFieldPartitioning` 
and 
`TestSimpleKeyGenerator.testSlashSeparatedDatePartitioningOnRowWritingPaths` 
already produce; the already-slashed input is a no-op `replace` duplicated by 
`testSlashSeparatedDatePartitioningWithAlreadyFormattedInput` in 
`TestSimpleKeyGenerator`, `TestComplexKeyGenerator`, `TestCustomKeyGenerator` 
and by `TestSlashSeparatedPartitionValue.scala:175`.



##########
hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/keygen/TestPartitionPathFormatter.java:
##########
@@ -0,0 +1,115 @@
+/*
+ * 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.hudi.keygen;
+
+import org.apache.spark.unsafe.types.UTF8String;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static 
org.apache.hudi.common.util.PartitionPathEncodeUtils.DEFAULT_PARTITION_PATH;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests the partition-path formatters backing the key generators, making sure 
that both the
+ * {@link String} (Avro/{@link org.apache.spark.sql.Row} write path) and the 
{@link UTF8String}
+ * (row-writer/{@link org.apache.spark.sql.catalyst.InternalRow} write path) 
flavors produce
+ * identical partition paths.
+ */
+class TestPartitionPathFormatter {
+
+  private static final List<String> SINGLE_FIELD = 
Collections.singletonList("date_col");
+  private static final List<String> TWO_FIELDS = Arrays.asList("date_col", 
"city");
+
+  private String combine(boolean useRowWriterPath,
+                         boolean hiveStylePartitioning,
+                         boolean encode,
+                         boolean slashSeparatedDatePartitioning,
+                         List<String> fields,
+                         Object... parts) {
+    if (useRowWriterPath) {
+      return new UTF8StringPartitionPathFormatter(
+          UTF8StringPartitionPathFormatter.UTF8StringBuilder::new, 
hiveStylePartitioning, encode,
+          slashSeparatedDatePartitioning).combine(fields, parts).toString();
+    }
+    return new StringPartitionPathFormatter(
+        StringPartitionPathFormatter.JavaStringBuilder::new, 
hiveStylePartitioning, encode,
+        slashSeparatedDatePartitioning).combine(fields, parts);
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void testSlashSeparatedDatePartitioningSingleField(boolean useRowWriterPath) 
{
+    assertEquals("2026/01/05",
+        combine(useRowWriterPath, false, false, true, SINGLE_FIELD, 
"2026-01-05"));
+    // Input that is already slash-separated is left untouched
+    assertEquals("2026/01/05",
+        combine(useRowWriterPath, false, false, true, SINGLE_FIELD, 
"2026/01/05"));
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void 
testSlashSeparatedDatePartitioningOnlyAppliesToSingleFieldPartitioning(boolean 
useRowWriterPath) {
+    // NOTE: This mirrors [[KeyGenUtils#getRecordPartitionPath]] driving the 
Avro write-path
+    assertEquals("2026-01-05/san-francisco",
+        combine(useRowWriterPath, false, false, true, TWO_FIELDS, 
"2026-01-05", "san-francisco"));
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void testSlashSeparatedDatePartitioningHandlesNullAndEmptyValues(boolean 
useRowWriterPath) {
+    assertEquals(DEFAULT_PARTITION_PATH,
+        combine(useRowWriterPath, false, false, true, SINGLE_FIELD, new 
Object[] {null}));
+    assertEquals(DEFAULT_PARTITION_PATH,
+        combine(useRowWriterPath, false, false, true, SINGLE_FIELD, ""));
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void testSlashSeparatedDatePartitioningEncodesValues(boolean 
useRowWriterPath) {
+    // '?' has to be escaped, while the date separators are turned into 
directory separators
+    assertEquals("2026/01/05", combine(useRowWriterPath, false, true, true, 
SINGLE_FIELD, "2026-01-05"));
+    assertEquals("a%3Fb", combine(useRowWriterPath, false, true, true, 
SINGLE_FIELD, "a?b"));

Review Comment:
   Pre-PR the slash branch skipped `tryEncode`, so the one input whose output 
actually changes under `urlencode=true` is an already slash-separated value 
(`2026/01/05` -> `2026%2F01%2F05`, matching 
`KeyGenUtils.getRecordPartitionPath:255-262` and fixing file-index prefix 
pruning on such tables). Both asserts here are no-ops for that change; pin it:
   ```suggestion
       assertEquals("a%3Fb", combine(useRowWriterPath, false, true, true, 
SINGLE_FIELD, "a?b"));
       // Encoding runs before the substitution (parity with KeyGenUtils), so 
an already slash-separated
       // value is escaped rather than turned into directories
       assertEquals("2026%2F01%2F05", combine(useRowWriterPath, false, true, 
true, SINGLE_FIELD, "2026/01/05"));
   ```



##########
hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/keygen/TestPartitionPathFormatter.java:
##########
@@ -0,0 +1,115 @@
+/*
+ * 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.hudi.keygen;
+
+import org.apache.spark.unsafe.types.UTF8String;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static 
org.apache.hudi.common.util.PartitionPathEncodeUtils.DEFAULT_PARTITION_PATH;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Tests the partition-path formatters backing the key generators, making sure 
that both the
+ * {@link String} (Avro/{@link org.apache.spark.sql.Row} write path) and the 
{@link UTF8String}
+ * (row-writer/{@link org.apache.spark.sql.catalyst.InternalRow} write path) 
flavors produce
+ * identical partition paths.
+ */
+class TestPartitionPathFormatter {
+
+  private static final List<String> SINGLE_FIELD = 
Collections.singletonList("date_col");
+  private static final List<String> TWO_FIELDS = Arrays.asList("date_col", 
"city");
+
+  private String combine(boolean useRowWriterPath,
+                         boolean hiveStylePartitioning,
+                         boolean encode,
+                         boolean slashSeparatedDatePartitioning,
+                         List<String> fields,
+                         Object... parts) {
+    if (useRowWriterPath) {
+      return new UTF8StringPartitionPathFormatter(
+          UTF8StringPartitionPathFormatter.UTF8StringBuilder::new, 
hiveStylePartitioning, encode,
+          slashSeparatedDatePartitioning).combine(fields, parts).toString();
+    }
+    return new StringPartitionPathFormatter(
+        StringPartitionPathFormatter.JavaStringBuilder::new, 
hiveStylePartitioning, encode,
+        slashSeparatedDatePartitioning).combine(fields, parts);
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void testSlashSeparatedDatePartitioningSingleField(boolean useRowWriterPath) 
{
+    assertEquals("2026/01/05",
+        combine(useRowWriterPath, false, false, true, SINGLE_FIELD, 
"2026-01-05"));
+    // Input that is already slash-separated is left untouched
+    assertEquals("2026/01/05",
+        combine(useRowWriterPath, false, false, true, SINGLE_FIELD, 
"2026/01/05"));
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void 
testSlashSeparatedDatePartitioningOnlyAppliesToSingleFieldPartitioning(boolean 
useRowWriterPath) {
+    // NOTE: This mirrors [[KeyGenUtils#getRecordPartitionPath]] driving the 
Avro write-path
+    assertEquals("2026-01-05/san-francisco",
+        combine(useRowWriterPath, false, false, true, TWO_FIELDS, 
"2026-01-05", "san-francisco"));
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void testSlashSeparatedDatePartitioningHandlesNullAndEmptyValues(boolean 
useRowWriterPath) {
+    assertEquals(DEFAULT_PARTITION_PATH,
+        combine(useRowWriterPath, false, false, true, SINGLE_FIELD, new 
Object[] {null}));
+    assertEquals(DEFAULT_PARTITION_PATH,
+        combine(useRowWriterPath, false, false, true, SINGLE_FIELD, ""));
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void testSlashSeparatedDatePartitioningEncodesValues(boolean 
useRowWriterPath) {
+    // '?' has to be escaped, while the date separators are turned into 
directory separators
+    assertEquals("2026/01/05", combine(useRowWriterPath, false, true, true, 
SINGLE_FIELD, "2026-01-05"));
+    assertEquals("a%3Fb", combine(useRowWriterPath, false, true, true, 
SINGLE_FIELD, "a?b"));
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void testHiveStylePartitioningTakesPrecedence(boolean useRowWriterPath) {
+    // NOTE: Hive-style partitioning and slash-separated date partitioning are 
mutually exclusive --
+    //       [[HoodieCatalogTable#extraTableConfig]] rejects a table 
configuring both -- so this
+    //       combination is unreachable and the formatter deliberately leaves 
the value alone.
+    //       This asserts the pre-existing behavior stays put, it is not a 
statement about what the
+    //       combination *should* produce
+    assertEquals("date_col=2026-01-05",
+        combine(useRowWriterPath, true, false, true, SINGLE_FIELD, 
"2026-01-05"));
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {true, false})
+  void testPlainPartitioningIsUnaffected(boolean useRowWriterPath) {

Review Comment:
   Delete: cannot fail on master, and both asserts are already pinned at Row 
and InternalRow level by `TestComplexKeyGenerator.testSingleValueKeyGenerator` 
(`:148/:151`) and `testMultipleValueKeyGenerator` (`:183/:187`).



##########
hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/keygen/TestPartitionPathFormatter.java:
##########
@@ -0,0 +1,115 @@
+/*
+ * 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.hudi.keygen;
+
+import org.apache.spark.unsafe.types.UTF8String;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static 
org.apache.hudi.common.util.PartitionPathEncodeUtils.DEFAULT_PARTITION_PATH;

Review Comment:
   nit, feel free to ignore: sibling tests in this package use 
`KeyGenUtils.HUDI_DEFAULT_PARTITION_PATH` (same constant, 
`KeyGenUtils.java:55`).



##########
hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestSlashSeparatedPartitionValue.scala:
##########
@@ -90,6 +90,61 @@ class TestSlashSeparatedPartitionValue extends 
HoodieSparkSqlTestBase {
     }
   }
 
+  test("Test slash separated date partitions written through the row writer") {
+    withSQLConf("hoodie.sql.bulk.insert.enable" -> "true", 
"hoodie.sql.insert.mode" -> "non-strict",
+      "hoodie.datasource.write.row.writer.enable" -> "true") {

Review Comment:
   `hoodie.sql.bulk.insert.enable` and `hoodie.sql.insert.mode` are 
`@Deprecated` / `deprecatedAfter("0.14.0")` (`DataSourceOptions.scala:569-584`) 
and `row.writer.enable=true` is the default; the single modern config below 
still routes to `bulkInsertAsRow` (verified red on master with the same CCE, 
green here).
   ```suggestion
       withSQLConf("hoodie.spark.sql.insert.into.operation" -> "bulk_insert") {
   ```
   nit, feel free to ignore: this is also the third copy of the DDL + 
metaClient block in this file -- folding into `"Test slash separated date 
partitions"` with `Seq("insert", "bulk_insert").foreach` plus the null row 
would drop ~40 lines.



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

Reply via email to