Skip to content

Commit ffa4d0c

Browse files
authored
feat(datafusion): support SHOW CREATE TABLE via get_table_definition (#444)
DataFusion 53 rewrites `SHOW CREATE TABLE` into a query against `information_schema.views`, whose `definition` column is populated by `TableProvider::get_table_definition()`. PaimonTableProvider did not override this method, so the column came back empty for Paimon tables. Add a cached DDL string built from the table's identifier, schema (fields, primary keys, partition keys, options), and a recursive `data_type_to_sql` renderer covering all DataType variants. Override `get_table_definition()` to return it.
1 parent b7f573b commit ffa4d0c

3 files changed

Lines changed: 791 additions & 37 deletions

File tree

crates/integrations/datafusion/src/sql_context.rs

Lines changed: 181 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,21 @@ use datafusion::error::{DataFusionError, Result as DFResult};
4949
use datafusion::execution::SessionStateBuilder;
5050
use datafusion::prelude::{DataFrame, SessionContext};
5151
use 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
};
5757
use datafusion::sql::sqlparser::dialect::GenericDialect;
5858
use datafusion::sql::sqlparser::parser::Parser;
5959
use futures::StreamExt;
6060
use paimon::catalog::{Catalog, Identifier};
6161
use 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

6869
use 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+
16301727
fn 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

Comments
 (0)