@@ -49,20 +49,21 @@ use datafusion::error::{DataFusionError, Result as DFResult};
4949use datafusion:: execution:: SessionStateBuilder ;
5050use datafusion:: prelude:: { DataFrame , SessionContext } ;
5151use datafusion:: sql:: sqlparser:: ast:: {
52- AlterTableOperation , ColumnDef , CreateTable , CreateTableOptions , CreateView , Delete ,
53- Expr as SqlExpr , FromTable , Insert , Merge , ObjectName , ObjectType , RenameTableNameKind , Reset ,
54- ResetStatement , Set , SqlOption , Statement , TableFactor , TableObject , Truncate , Update ,
55- Value as SqlValue ,
52+ AlterTableOperation , BinaryLength , CharacterLength , ColumnDef , CreateTable , CreateTableOptions ,
53+ CreateView , Delete , Expr as SqlExpr , FromTable , Insert , Merge , ObjectName , ObjectType ,
54+ RenameTableNameKind , Reset , ResetStatement , Set , SqlOption , Statement , TableFactor ,
55+ TableObject , Truncate , Update , Value as SqlValue ,
5656} ;
5757use datafusion:: sql:: sqlparser:: dialect:: GenericDialect ;
5858use datafusion:: sql:: sqlparser:: parser:: Parser ;
5959use futures:: StreamExt ;
6060use paimon:: catalog:: { Catalog , Identifier } ;
6161use paimon:: spec:: {
62- ArrayType as PaimonArrayType , BigIntType , BlobType , BooleanType , DataField as PaimonDataField ,
63- DataType as PaimonDataType , DateType , Datum , DecimalType , DoubleType , FloatType , IntType ,
64- LocalZonedTimestampType , MapType as PaimonMapType , RowType as PaimonRowType , SchemaChange ,
65- SmallIntType , TimestampType , TinyIntType , VarBinaryType , VarCharType , VariantType ,
62+ ArrayType as PaimonArrayType , BigIntType , BinaryType , BlobType , BooleanType , CharType ,
63+ DataField as PaimonDataField , DataType as PaimonDataType , DateType , Datum , DecimalType ,
64+ DoubleType , FloatType , IntType , LocalZonedTimestampType , MapType as PaimonMapType ,
65+ RowType as PaimonRowType , SchemaChange , SmallIntType , TimestampType , TinyIntType ,
66+ VarBinaryType , VarCharType , VariantType ,
6667} ;
6768
6869use crate :: error:: to_datafusion_error;
@@ -682,7 +683,7 @@ impl SQLContext {
682683 let pk_cols: Vec < String > = pk
683684 . columns
684685 . iter ( )
685- . map ( |c| c. column . expr . to_string ( ) )
686+ . map ( |c| primary_key_column_name ( & c. column . expr ) )
686687 . collect ( ) ;
687688 builder = builder. primary_key ( pk_cols) ;
688689 }
@@ -1532,11 +1533,25 @@ fn parse_partition_column(token: &str) -> DFResult<String> {
15321533
15331534 let first = trimmed. as_bytes ( ) [ 0 ] ;
15341535 if first == b'"' || first == b'`' {
1535- let close = if first == b'"' { b'"' } else { b'`' } ;
1536- if let Some ( end) = trimmed[ 1 ..] . find ( close as char ) {
1537- let after_quote = trimmed[ 1 + end + 1 ..] . trim ( ) ;
1538- if after_quote. is_empty ( ) {
1539- return Ok ( trimmed[ 1 ..1 + end] . to_string ( ) ) ;
1536+ let mut value = String :: new ( ) ;
1537+ let mut end = None ;
1538+ let mut chars = trimmed[ 1 ..] . char_indices ( ) . peekable ( ) ;
1539+ while let Some ( ( idx, ch) ) = chars. next ( ) {
1540+ if ch == first as char {
1541+ if chars. peek ( ) . is_some_and ( |( _, next) | * next == first as char ) {
1542+ value. push ( ch) ;
1543+ chars. next ( ) ;
1544+ } else {
1545+ end = Some ( 1 + idx + ch. len_utf8 ( ) ) ;
1546+ break ;
1547+ }
1548+ } else {
1549+ value. push ( ch) ;
1550+ }
1551+ }
1552+ if let Some ( end) = end {
1553+ if trimmed[ end..] . trim ( ) . is_empty ( ) {
1554+ return Ok ( value) ;
15401555 }
15411556 }
15421557 return Err ( DataFusionError :: Plan ( format ! (
@@ -1555,6 +1570,38 @@ fn parse_partition_column(token: &str) -> DFResult<String> {
15551570 }
15561571}
15571572
1573+ fn split_partition_columns ( inner : & str ) -> DFResult < Vec < & str > > {
1574+ let mut columns = Vec :: new ( ) ;
1575+ let mut start = 0 ;
1576+ let mut quote = None ;
1577+ let mut chars = inner. char_indices ( ) . peekable ( ) ;
1578+ while let Some ( ( idx, ch) ) = chars. next ( ) {
1579+ match quote {
1580+ Some ( q) if ch == q => {
1581+ if chars. peek ( ) . is_some_and ( |( _, next) | * next == q) {
1582+ chars. next ( ) ;
1583+ } else {
1584+ quote = None ;
1585+ }
1586+ }
1587+ Some ( _) => { }
1588+ None if ch == '"' || ch == '`' => quote = Some ( ch) ,
1589+ None if ch == ',' => {
1590+ columns. push ( & inner[ start..idx] ) ;
1591+ start = idx + ch. len_utf8 ( ) ;
1592+ }
1593+ None => { }
1594+ }
1595+ }
1596+ if quote. is_some ( ) {
1597+ return Err ( DataFusionError :: Plan (
1598+ "Unterminated quoted identifier in PARTITIONED BY" . to_string ( ) ,
1599+ ) ) ;
1600+ }
1601+ columns. push ( & inner[ start..] ) ;
1602+ Ok ( columns)
1603+ }
1604+
15581605/// Extract `PARTITIONED BY (col1, col2, ...)` from SQL before parsing.
15591606///
15601607/// Paimon only allows column references (no types) in PARTITIONED BY.
@@ -1578,17 +1625,28 @@ fn extract_partition_by(sql: &str) -> DFResult<(String, Vec<String>)> {
15781625 let inner_start = paren_start + 1 ;
15791626 let mut depth = 1 ;
15801627 let mut paren_end = None ;
1581- for ( i, ch) in sql[ inner_start..] . char_indices ( ) {
1582- match ch {
1583- '(' => depth += 1 ,
1584- ')' => {
1628+ let mut quote = None ;
1629+ let mut chars = sql[ inner_start..] . char_indices ( ) . peekable ( ) ;
1630+ while let Some ( ( i, ch) ) = chars. next ( ) {
1631+ match quote {
1632+ Some ( q) if ch == q => {
1633+ if chars. peek ( ) . is_some_and ( |( _, next) | * next == q) {
1634+ chars. next ( ) ;
1635+ } else {
1636+ quote = None ;
1637+ }
1638+ }
1639+ Some ( _) => { }
1640+ None if ch == '"' || ch == '`' => quote = Some ( ch) ,
1641+ None if ch == '(' => depth += 1 ,
1642+ None if ch == ')' => {
15851643 depth -= 1 ;
15861644 if depth == 0 {
15871645 paren_end = Some ( inner_start + i) ;
15881646 break ;
15891647 }
15901648 }
1591- _ => { }
1649+ None => { }
15921650 }
15931651 }
15941652 let paren_end = paren_end. ok_or_else ( || {
@@ -1603,7 +1661,7 @@ fn extract_partition_by(sql: &str) -> DFResult<(String, Vec<String>)> {
16031661 }
16041662
16051663 let mut partition_keys = Vec :: new ( ) ;
1606- for token in inner . split ( ',' ) {
1664+ for token in split_partition_columns ( inner ) ? {
16071665 partition_keys. push ( parse_partition_column ( token) ?) ;
16081666 }
16091667
@@ -1627,6 +1685,45 @@ fn column_def_to_paimon_type(col: &ColumnDef) -> DFResult<PaimonDataType> {
16271685 sql_data_type_to_paimon_type ( & col. data_type , column_def_nullable ( col) )
16281686}
16291687
1688+ fn primary_key_column_name ( expr : & SqlExpr ) -> String {
1689+ match expr {
1690+ SqlExpr :: Identifier ( ident) => ident. value . clone ( ) ,
1691+ _ => expr. to_string ( ) ,
1692+ }
1693+ }
1694+
1695+ fn character_length_or_default (
1696+ length : & Option < CharacterLength > ,
1697+ default_length : u32 ,
1698+ ) -> DFResult < u32 > {
1699+ match length {
1700+ Some ( CharacterLength :: IntegerLength { length, .. } ) => ( * length) . try_into ( ) . map_err ( |_| {
1701+ DataFusionError :: Plan ( format ! ( "Character length {length} exceeds supported range" ) )
1702+ } ) ,
1703+ Some ( CharacterLength :: Max ) => Ok ( VarCharType :: MAX_LENGTH ) ,
1704+ None => Ok ( default_length) ,
1705+ }
1706+ }
1707+
1708+ fn u64_length_or_default ( length : Option < u64 > , default_length : usize ) -> DFResult < usize > {
1709+ match length {
1710+ Some ( length) => length. try_into ( ) . map_err ( |_| {
1711+ DataFusionError :: Plan ( format ! ( "Binary length {length} exceeds supported range" ) )
1712+ } ) ,
1713+ None => Ok ( default_length) ,
1714+ }
1715+ }
1716+
1717+ fn binary_length_or_default ( length : & Option < BinaryLength > , default_length : u32 ) -> DFResult < u32 > {
1718+ match length {
1719+ Some ( BinaryLength :: IntegerLength { length } ) => ( * length) . try_into ( ) . map_err ( |_| {
1720+ DataFusionError :: Plan ( format ! ( "Binary length {length} exceeds supported range" ) )
1721+ } ) ,
1722+ Some ( BinaryLength :: Max ) => Ok ( VarBinaryType :: MAX_LENGTH ) ,
1723+ None => Ok ( default_length) ,
1724+ }
1725+ }
1726+
16301727fn column_def_nullable ( col : & ColumnDef ) -> bool {
16311728 !col. options . iter ( ) . any ( |opt| {
16321729 matches ! (
@@ -1668,21 +1765,39 @@ fn sql_data_type_to_paimon_type(
16681765 SqlType :: Double ( _) | SqlType :: DoublePrecision => {
16691766 Ok ( PaimonDataType :: Double ( DoubleType :: with_nullable ( nullable) ) )
16701767 }
1671- SqlType :: Varchar ( _)
1672- | SqlType :: CharVarying ( _)
1673- | SqlType :: Text
1674- | SqlType :: String ( _)
1675- | SqlType :: Char ( _)
1676- | SqlType :: Character ( _) => Ok ( PaimonDataType :: VarChar (
1768+ SqlType :: Char ( length) | SqlType :: Character ( length) => Ok ( PaimonDataType :: Char (
1769+ CharType :: with_nullable ( nullable, character_length_or_default ( length, 1 ) ? as usize )
1770+ . map_err ( to_datafusion_error) ?,
1771+ ) ) ,
1772+ SqlType :: Varchar ( length)
1773+ | SqlType :: Nvarchar ( length)
1774+ | SqlType :: CharVarying ( length)
1775+ | SqlType :: CharacterVarying ( length) => Ok ( PaimonDataType :: VarChar (
1776+ VarCharType :: with_nullable (
1777+ nullable,
1778+ character_length_or_default ( length, VarCharType :: MAX_LENGTH ) ?,
1779+ )
1780+ . map_err ( to_datafusion_error) ?,
1781+ ) ) ,
1782+ SqlType :: Text | SqlType :: String ( _) => Ok ( PaimonDataType :: VarChar (
16771783 VarCharType :: with_nullable ( nullable, VarCharType :: MAX_LENGTH )
16781784 . map_err ( to_datafusion_error) ?,
16791785 ) ) ,
1680- SqlType :: Binary ( _) | SqlType :: Varbinary ( _) | SqlType :: Bytea => {
1681- Ok ( PaimonDataType :: VarBinary (
1682- VarBinaryType :: try_new ( nullable, VarBinaryType :: MAX_LENGTH )
1683- . map_err ( to_datafusion_error) ?,
1684- ) )
1685- }
1786+ SqlType :: Binary ( length) => Ok ( PaimonDataType :: Binary (
1787+ BinaryType :: with_nullable ( nullable, u64_length_or_default ( * length, 1 ) ? as usize )
1788+ . map_err ( to_datafusion_error) ?,
1789+ ) ) ,
1790+ SqlType :: Varbinary ( length) => Ok ( PaimonDataType :: VarBinary (
1791+ VarBinaryType :: try_new (
1792+ nullable,
1793+ binary_length_or_default ( length, VarBinaryType :: MAX_LENGTH ) ?,
1794+ )
1795+ . map_err ( to_datafusion_error) ?,
1796+ ) ) ,
1797+ SqlType :: Bytea => Ok ( PaimonDataType :: VarBinary (
1798+ VarBinaryType :: try_new ( nullable, VarBinaryType :: MAX_LENGTH )
1799+ . map_err ( to_datafusion_error) ?,
1800+ ) ) ,
16861801 SqlType :: Blob ( _) => Ok ( PaimonDataType :: Blob ( BlobType :: with_nullable ( nullable) ) ) ,
16871802 SqlType :: Custom ( name, modifiers)
16881803 if name. to_string ( ) . eq_ignore_ascii_case ( "VARIANT" ) && modifiers. is_empty ( ) =>
@@ -2822,7 +2937,7 @@ mod tests {
28222937
28232938 #[ test]
28242939 fn test_sql_type_string_variants ( ) {
2825- use datafusion:: sql:: sqlparser:: ast:: DataType as SqlType ;
2940+ use datafusion:: sql:: sqlparser:: ast:: { CharacterLength , DataType as SqlType } ;
28262941 for sql_type in [ SqlType :: Varchar ( None ) , SqlType :: Text , SqlType :: String ( None ) ] {
28272942 assert_sql_type_to_paimon (
28282943 sql_type. clone ( ) ,
@@ -2831,17 +2946,39 @@ mod tests {
28312946 ) ,
28322947 ) ;
28332948 }
2949+ assert_sql_type_to_paimon (
2950+ SqlType :: Char ( Some ( CharacterLength :: IntegerLength {
2951+ length : 7 ,
2952+ unit : None ,
2953+ } ) ) ,
2954+ PaimonDataType :: Char ( CharType :: with_nullable ( true , 7 ) . unwrap ( ) ) ,
2955+ ) ;
2956+ assert_sql_type_to_paimon (
2957+ SqlType :: Varchar ( Some ( CharacterLength :: IntegerLength {
2958+ length : 42 ,
2959+ unit : None ,
2960+ } ) ) ,
2961+ PaimonDataType :: VarChar ( VarCharType :: with_nullable ( true , 42 ) . unwrap ( ) ) ,
2962+ ) ;
28342963 }
28352964
28362965 #[ test]
28372966 fn test_sql_type_binary ( ) {
2838- use datafusion:: sql:: sqlparser:: ast:: DataType as SqlType ;
2967+ use datafusion:: sql:: sqlparser:: ast:: { BinaryLength , DataType as SqlType } ;
28392968 assert_sql_type_to_paimon (
28402969 SqlType :: Bytea ,
28412970 PaimonDataType :: VarBinary (
28422971 VarBinaryType :: try_new ( true , VarBinaryType :: MAX_LENGTH ) . unwrap ( ) ,
28432972 ) ,
28442973 ) ;
2974+ assert_sql_type_to_paimon (
2975+ SqlType :: Binary ( Some ( 8 ) ) ,
2976+ PaimonDataType :: Binary ( BinaryType :: with_nullable ( true , 8 ) . unwrap ( ) ) ,
2977+ ) ;
2978+ assert_sql_type_to_paimon (
2979+ SqlType :: Varbinary ( Some ( BinaryLength :: IntegerLength { length : 32 } ) ) ,
2980+ PaimonDataType :: VarBinary ( VarBinaryType :: try_new ( true , 32 ) . unwrap ( ) ) ,
2981+ ) ;
28452982 }
28462983
28472984 #[ test]
@@ -3558,6 +3695,16 @@ mod tests {
35583695 assert_eq ! ( keys, vec![ "order" ] ) ;
35593696 }
35603697
3698+ #[ test]
3699+ fn test_extract_partition_by_double_quoted_identifier_with_escaped_quote_and_comma ( ) {
3700+ let ( _, keys) = extract_partition_by (
3701+ "CREATE TABLE t (\" a\" \" b,c\" INT, `d``e,f` INT) \
3702+ PARTITIONED BY (\" a\" \" b,c\" , `d``e,f`)",
3703+ )
3704+ . unwrap ( ) ;
3705+ assert_eq ! ( keys, vec![ "a\" b,c" , "d`e,f" ] ) ;
3706+ }
3707+
35613708 #[ test]
35623709 fn test_extract_partition_by_backtick_quoted_identifier ( ) {
35633710 let ( _, keys) =
0 commit comments