This is an automated email from the ASF dual-hosted git repository.

deniskuzZ pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new 0a4ff942fea HIVE-29749: TypeInfoUtils fails to parse struct type 
strings with special characters in field names (#6619)
0a4ff942fea is described below

commit 0a4ff942fea99722342f4901e877ab10be76a18f
Author: Archit Jain <[email protected]>
AuthorDate: Wed Jul 22 17:54:02 2026 +0530

    HIVE-29749: TypeInfoUtils fails to parse struct type strings with special 
characters in field names (#6619)
---
 .../positive/iceberg_partition_special_chars.q     | 31 +++++++
 .../positive/iceberg_partition_special_chars.q.out | 97 ++++++++++++++++++++++
 .../hadoop/hive/serde2/typeinfo/TypeInfoUtils.java | 46 ++++++++--
 .../hive/serde2/typeinfo/TestTypeInfoUtils.java    | 48 ++++++++++-
 4 files changed, 213 insertions(+), 9 deletions(-)

diff --git 
a/iceberg/iceberg-handler/src/test/queries/positive/iceberg_partition_special_chars.q
 
b/iceberg/iceberg-handler/src/test/queries/positive/iceberg_partition_special_chars.q
new file mode 100644
index 00000000000..f117eb1f97c
--- /dev/null
+++ 
b/iceberg/iceberg-handler/src/test/queries/positive/iceberg_partition_special_chars.q
@@ -0,0 +1,31 @@
+CREATE TABLE studenttab10k(
+ name string,
+ age int,
+ gpa double
+)
+ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
+STORED AS TEXTFILE;
+
+LOAD DATA LOCAL INPATH '../../data/files/studenttab10k' OVERWRITE INTO TABLE 
studenttab10k;
+
+
+set hive.support.quoted.identifiers=column;
+
+drop table if exists quoted_identifier_2_1;
+
+create table quoted_identifier_2_1(`-+={[<_name_>]}?/|` string, 
`!@#$%^&*()_age` int) partitioned by(`gpa_!@#$%^&*()` double) stored by iceberg;
+
+insert into table quoted_identifier_2_1 select name,age,gpa from studenttab10k 
where gpa=3.5;
+
+drop view if exists quoted_identifier_2_2;
+
+create view quoted_identifier_2_2 as select `!@#$%^&*()_age`,count(*)  from 
quoted_identifier_2_1 group by `!@#$%^&*()_age`;
+
+-- Two partition columns: one plain (age), one with special characters 
(gpa_!@#$%^&*())
+drop table if exists quoted_identifier_2_3;
+
+create table quoted_identifier_2_3(`name_col` string) partitioned by(age int, 
`gpa_!@#$%^&*()` double) stored by iceberg;
+
+insert into table quoted_identifier_2_3 select name,age,gpa from studenttab10k 
where gpa=4.5;
+
+select count(*) from quoted_identifier_2_3;
diff --git 
a/iceberg/iceberg-handler/src/test/results/positive/iceberg_partition_special_chars.q.out
 
b/iceberg/iceberg-handler/src/test/results/positive/iceberg_partition_special_chars.q.out
new file mode 100644
index 00000000000..c8f2da301e4
--- /dev/null
+++ 
b/iceberg/iceberg-handler/src/test/results/positive/iceberg_partition_special_chars.q.out
@@ -0,0 +1,97 @@
+PREHOOK: query: CREATE TABLE studenttab10k(
+ name string,
+ age int,
+ gpa double
+)
+ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
+STORED AS TEXTFILE
+PREHOOK: type: CREATETABLE
+PREHOOK: Output: database:default
+PREHOOK: Output: default@studenttab10k
+POSTHOOK: query: CREATE TABLE studenttab10k(
+ name string,
+ age int,
+ gpa double
+)
+ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
+STORED AS TEXTFILE
+POSTHOOK: type: CREATETABLE
+POSTHOOK: Output: database:default
+POSTHOOK: Output: default@studenttab10k
+PREHOOK: query: LOAD DATA LOCAL INPATH '../../data/files/studenttab10k' 
OVERWRITE INTO TABLE studenttab10k
+PREHOOK: type: LOAD
+#### A masked pattern was here ####
+PREHOOK: Output: default@studenttab10k
+POSTHOOK: query: LOAD DATA LOCAL INPATH '../../data/files/studenttab10k' 
OVERWRITE INTO TABLE studenttab10k
+POSTHOOK: type: LOAD
+#### A masked pattern was here ####
+POSTHOOK: Output: default@studenttab10k
+PREHOOK: query: drop table if exists quoted_identifier_2_1
+PREHOOK: type: DROPTABLE
+PREHOOK: Output: database:default
+POSTHOOK: query: drop table if exists quoted_identifier_2_1
+POSTHOOK: type: DROPTABLE
+POSTHOOK: Output: database:default
+PREHOOK: query: create table quoted_identifier_2_1(`-+={[<_name_>]}?/|` 
string, `!@#$%^&*()_age` int) partitioned by(`gpa_!@#$%^&*()` double) stored by 
iceberg
+PREHOOK: type: CREATETABLE
+PREHOOK: Output: database:default
+PREHOOK: Output: default@quoted_identifier_2_1
+POSTHOOK: query: create table quoted_identifier_2_1(`-+={[<_name_>]}?/|` 
string, `!@#$%^&*()_age` int) partitioned by(`gpa_!@#$%^&*()` double) stored by 
iceberg
+POSTHOOK: type: CREATETABLE
+POSTHOOK: Output: database:default
+POSTHOOK: Output: default@quoted_identifier_2_1
+PREHOOK: query: insert into table quoted_identifier_2_1 select name,age,gpa 
from studenttab10k where gpa=3.5
+PREHOOK: type: QUERY
+PREHOOK: Input: default@studenttab10k
+PREHOOK: Output: default@quoted_identifier_2_1
+POSTHOOK: query: insert into table quoted_identifier_2_1 select name,age,gpa 
from studenttab10k where gpa=3.5
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@studenttab10k
+POSTHOOK: Output: default@quoted_identifier_2_1
+PREHOOK: query: drop view if exists quoted_identifier_2_2
+PREHOOK: type: DROPVIEW
+POSTHOOK: query: drop view if exists quoted_identifier_2_2
+POSTHOOK: type: DROPVIEW
+PREHOOK: query: create view quoted_identifier_2_2 as select 
`!@#$%^&*()_age`,count(*)  from quoted_identifier_2_1 group by `!@#$%^&*()_age`
+PREHOOK: type: CREATEVIEW
+PREHOOK: Input: default@quoted_identifier_2_1
+PREHOOK: Output: database:default
+PREHOOK: Output: default@quoted_identifier_2_2
+POSTHOOK: query: create view quoted_identifier_2_2 as select 
`!@#$%^&*()_age`,count(*)  from quoted_identifier_2_1 group by `!@#$%^&*()_age`
+POSTHOOK: type: CREATEVIEW
+POSTHOOK: Input: default@quoted_identifier_2_1
+POSTHOOK: Output: database:default
+POSTHOOK: Output: default@quoted_identifier_2_2
+POSTHOOK: Lineage: quoted_identifier_2_2.!@#$%^&*()_age SIMPLE 
[(quoted_identifier_2_1)quoted_identifier_2_1.FieldSchema(name:!@#$%^&*()_age, 
type:int, comment:null), ]
+POSTHOOK: Lineage: quoted_identifier_2_2._c1 EXPRESSION 
[(quoted_identifier_2_1)quoted_identifier_2_1.null, ]
+PREHOOK: query: drop table if exists quoted_identifier_2_3
+PREHOOK: type: DROPTABLE
+PREHOOK: Output: database:default
+POSTHOOK: query: drop table if exists quoted_identifier_2_3
+POSTHOOK: type: DROPTABLE
+POSTHOOK: Output: database:default
+PREHOOK: query: create table quoted_identifier_2_3(`name_col` string) 
partitioned by(age int, `gpa_!@#$%^&*()` double) stored by iceberg
+PREHOOK: type: CREATETABLE
+PREHOOK: Output: database:default
+PREHOOK: Output: default@quoted_identifier_2_3
+POSTHOOK: query: create table quoted_identifier_2_3(`name_col` string) 
partitioned by(age int, `gpa_!@#$%^&*()` double) stored by iceberg
+POSTHOOK: type: CREATETABLE
+POSTHOOK: Output: database:default
+POSTHOOK: Output: default@quoted_identifier_2_3
+PREHOOK: query: insert into table quoted_identifier_2_3 select name,age,gpa 
from studenttab10k where gpa=4.5
+PREHOOK: type: QUERY
+PREHOOK: Input: default@studenttab10k
+PREHOOK: Output: default@quoted_identifier_2_3
+POSTHOOK: query: insert into table quoted_identifier_2_3 select name,age,gpa 
from studenttab10k where gpa=4.5
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@studenttab10k
+POSTHOOK: Output: default@quoted_identifier_2_3
+PREHOOK: query: select count(*) from quoted_identifier_2_3
+PREHOOK: type: QUERY
+PREHOOK: Input: default@quoted_identifier_2_3
+PREHOOK: Output: hdfs://### HDFS PATH ###
+POSTHOOK: query: select count(*) from quoted_identifier_2_3
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@quoted_identifier_2_3
+POSTHOOK: Output: hdfs://### HDFS PATH ###
+0
diff --git 
a/serde/src/java/org/apache/hadoop/hive/serde2/typeinfo/TypeInfoUtils.java 
b/serde/src/java/org/apache/hadoop/hive/serde2/typeinfo/TypeInfoUtils.java
index ef4bdd2ae7f..f4fbedd6cd4 100644
--- a/serde/src/java/org/apache/hadoop/hive/serde2/typeinfo/TypeInfoUtils.java
+++ b/serde/src/java/org/apache/hadoop/hive/serde2/typeinfo/TypeInfoUtils.java
@@ -296,6 +296,14 @@ private static boolean isTypeChar(char c) {
       return Character.isLetterOrDigit(c) || c == '_' || c == '.' || c == ' ' 
|| c == '$';
     }
 
+    private static Token createToken(String typeInfoString, int start, int 
end) {
+      Token t = new Token();
+      t.position = start;
+      t.text = typeInfoString.substring(start, end).trim();
+      t.isType = isTypeChar(typeInfoString.charAt(start));
+      return t;
+    }
+
     /**
      * Tokenize the typeInfoString. The rule is simple: all consecutive
      * alphadigits and '_', '.' are in one token, and all other characters are
@@ -333,11 +341,7 @@ private static ArrayList<Token> tokenize(String 
typeInfoString) {
         if (end == typeInfoString.length()
             || !isTypeChar(typeInfoString.charAt(end - 1))
             || !isTypeChar(typeInfoString.charAt(end))) {
-          Token t = new Token();
-          t.position = begin;
-          t.text = typeInfoString.substring(begin, end).trim();
-          t.isType = isTypeChar(typeInfoString.charAt(begin));
-          tokens.add(t);
+          tokens.add(createToken(typeInfoString, begin, end));
           begin = end;
         }
         end++;
@@ -345,6 +349,7 @@ private static ArrayList<Token> tokenize(String 
typeInfoString) {
       return tokens;
     }
 
+
     public TypeInfoParser(String typeInfoString) {
       this.typeInfoString = typeInfoString;
       typeInfoTokens = tokenize(typeInfoString);
@@ -446,6 +451,31 @@ private String[] parseParams() {
       return params.toArray(new String[params.size()]);
     }
 
+    private static boolean isStructFieldNameEnd(Token t) {
+      return t == null || t.text.equals(":") || t.text.equals(">") || 
t.text.equals(",");
+    }
+
+    /**
+     * A struct field name can be split across multiple tokens, e.g. an Iceberg
+     * partition column name like "gpa_!@#$%^&amp;*()" is tokenized as "gpa_", 
"!",
+     * "@", ... since only letters/digits/'_'/'.'/' '/'$' form a single token.
+     * Consume tokens until the next structural delimiter: ":" ends the name,
+     * while ">" or "," with nothing consumed means there was no name (end of
+     * struct, or a trailing separator before the close).
+     */
+    private String parseStructFieldName() {
+      StringBuilder fieldName = new StringBuilder();
+      for (Token next = peek(); !isStructFieldNameEnd(next); next = peek()) {
+        fieldName.append(next.text);
+        iToken++;
+      }
+      if (fieldName.isEmpty()) {
+        expect(">");
+        return null;
+      }
+      return fieldName.toString();
+    }
+
     private TypeInfo parseType() {
 
       Token t = expect("type");
@@ -543,11 +573,11 @@ private TypeInfo parseType() {
               break;
             }
           }
-          Token name = expect("name",">");
-          if (name.text.equals(">")) {
+          String fieldName = parseStructFieldName();
+          if (fieldName == null) {
             break;
           }
-          fieldNames.add(name.text);
+          fieldNames.add(fieldName);
           expect(":");
           fieldTypeInfos.add(parseType());
         } while (true);
diff --git 
a/serde/src/test/org/apache/hadoop/hive/serde2/typeinfo/TestTypeInfoUtils.java 
b/serde/src/test/org/apache/hadoop/hive/serde2/typeinfo/TestTypeInfoUtils.java
index a29f5d1fe66..be8494ebc94 100644
--- 
a/serde/src/test/org/apache/hadoop/hive/serde2/typeinfo/TestTypeInfoUtils.java
+++ 
b/serde/src/test/org/apache/hadoop/hive/serde2/typeinfo/TestTypeInfoUtils.java
@@ -20,6 +20,7 @@
 
 
 
+import java.util.Arrays;
 import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo;
 import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoUtils;
 import static org.junit.Assert.assertEquals;
@@ -52,7 +53,13 @@ public void testTypeInfoParser() {
         "decimal(10, 2)",
         "decimal(10, 2 )",
         "decimal( 10, 2 )",
-        "struct<user id:int,user group: int>"
+        "struct<user id:int,user group: int>",
+        // struct field names with special characters (Iceberg partition 
column names)
+        "struct<gpa_!@#$%^&*():double>",
+        "struct<!@#$%^&*()_age:int>",
+        "struct<name:string,!@#$%^&*()_age:int,gpa_!@#$%^&*():double>",
+        "struct<name:string,gpa_!@#$%^&*():double>",
+        "struct<gpa_!@#$%^&*():double,age:int>"
     };
 
     String[] invalidTypeStrings = {
@@ -125,4 +132,43 @@ public void testDecimal() {
       assertEquals("Failed for " + testCase.typeString, 
testCase.expectedScale, decimalType.getScale());
     }
   }
+
+  @Test
+  public void testStructFieldNameWithSpecialChars() {
+    TypeInfo typeInfo = 
TypeInfoUtils.getTypeInfoFromTypeString("struct<gpa_!@#$%^&*():double>");
+    StructTypeInfo structTypeInfo = (StructTypeInfo) typeInfo;
+    assertEquals(1, structTypeInfo.getAllStructFieldNames().size());
+    assertEquals("gpa_!@#$%^&*()", 
structTypeInfo.getAllStructFieldNames().get(0));
+    assertEquals(TypeInfoFactory.doubleTypeInfo, 
structTypeInfo.getAllStructFieldTypeInfos().get(0));
+  }
+
+  @Test
+  public void testStructFieldNameStartingWithSpecialChar() {
+    TypeInfo typeInfo = 
TypeInfoUtils.getTypeInfoFromTypeString("struct<!@#$%^&*()_age:int>");
+    StructTypeInfo structTypeInfo = (StructTypeInfo) typeInfo;
+    assertEquals(Arrays.asList("!@#$%^&*()_age"), 
structTypeInfo.getAllStructFieldNames());
+  }
+
+  @Test
+  public void testStructMultipleFieldsWithSpecialChars() {
+    TypeInfo typeInfo = TypeInfoUtils.getTypeInfoFromTypeString(
+        "struct<name:string,!@#$%^&*()_age:int,gpa_!@#$%^&*():double>");
+    StructTypeInfo structTypeInfo = (StructTypeInfo) typeInfo;
+    assertEquals(Arrays.asList("name", "!@#$%^&*()_age", "gpa_!@#$%^&*()"),
+        structTypeInfo.getAllStructFieldNames());
+  }
+
+  @Test
+  public void testEmptyStruct() {
+    TypeInfo typeInfo = TypeInfoUtils.getTypeInfoFromTypeString("struct<>");
+    StructTypeInfo structTypeInfo = (StructTypeInfo) typeInfo;
+    assertEquals(0, structTypeInfo.getAllStructFieldNames().size());
+  }
+
+  @Test
+  public void testStructTrailingComma() {
+    TypeInfo typeInfo = 
TypeInfoUtils.getTypeInfoFromTypeString("struct<a:int,>");
+    StructTypeInfo structTypeInfo = (StructTypeInfo) typeInfo;
+    assertEquals(Arrays.asList("a"), structTypeInfo.getAllStructFieldNames());
+  }
 }

Reply via email to