Skip to content

Commit 1b0e605

Browse files
authored
feat(datafusion): upgrade to DataFusion 53 and use VERSION AS OF (#236)
Upgrade DataFusion from 52.3 to 53.0 (arrow/parquet 57→58, sqlparser 0.59→0.61, orc-rust 0.7→0.8, pyo3 0.26→0.28) and replace the old `FOR SYSTEM_TIME AS OF` time travel syntax with the new `VERSION AS OF` and `TIMESTAMP AS OF` syntax supported by sqlparser 0.61. Introduce `scan.version` option to unify snapshot id and tag name resolution: at scan time, the version value is resolved by first checking if a tag with that name exists, then trying to parse it as a snapshot id, otherwise returning an error. Remove the now-redundant `scan.snapshot-id` and `scan.tag-name` options.
1 parent 7b3c89f commit 1b0e605

14 files changed

Lines changed: 211 additions & 237 deletions

File tree

Cargo.toml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ license = "Apache-2.0"
2828
rust-version = "1.86.0"
2929

3030
[workspace.dependencies]
31-
arrow = "57.0"
32-
arrow-array = { version = "57.0", features = ["ffi"] }
33-
arrow-buffer = "57.0"
34-
arrow-schema = "57.0"
35-
arrow-cast = "57.0"
36-
arrow-ord = "57.0"
37-
arrow-select = "57.0"
38-
datafusion = "52.3.0"
39-
datafusion-ffi = "52.3.0"
40-
parquet = "57.0"
31+
arrow = "58.0"
32+
arrow-array = { version = "58.0", features = ["ffi"] }
33+
arrow-buffer = "58.0"
34+
arrow-schema = "58.0"
35+
arrow-cast = "58.0"
36+
arrow-ord = "58.0"
37+
arrow-select = "58.0"
38+
datafusion = "53.0.0"
39+
datafusion-ffi = "53.0.0"
40+
parquet = "58.0"
4141
tokio = "1.39.2"
4242
tokio-util = "0.7"

bindings/python/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ datafusion = { workspace = true }
3232
datafusion-ffi = { workspace = true }
3333
paimon = { path = "../../crates/paimon", features = ["storage-all"] }
3434
paimon-datafusion = { path = "../../crates/integrations/datafusion" }
35-
pyo3 = { version = "0.26", features = ["abi3-py310"] }
35+
pyo3 = { version = "0.28", features = ["abi3-py310"] }
3636
tokio = { workspace = true }

bindings/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,5 @@ dev = [
5353
"maturin>=1.9.4,<2.0",
5454
"pytest>=8.0",
5555
"pyarrow>=17.0",
56-
"datafusion==52.3.0",
56+
"datafusion==53.0.0",
5757
]

bindings/python/src/context.rs

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@
1616
// under the License.
1717

1818
use std::collections::HashMap;
19-
use std::ptr::NonNull;
2019
use std::sync::Arc;
2120

2221
use datafusion::catalog::CatalogProvider;
2322
use datafusion_ffi::catalog_provider::FFI_CatalogProvider;
2423
use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec;
2524
use paimon::{CatalogFactory, Options};
2625
use paimon_datafusion::PaimonCatalogProvider;
27-
use pyo3::exceptions::PyValueError;
2826
use pyo3::prelude::*;
2927
use pyo3::types::PyCapsule;
3028

@@ -52,23 +50,8 @@ fn ffi_logical_codec_from_pycapsule(obj: Bound<'_, PyAny>) -> PyResult<FFI_Logic
5250

5351
let capsule = capsule.cast::<PyCapsule>()?;
5452
let expected_name = c"datafusion_logical_extension_codec";
55-
match capsule.name()? {
56-
Some(name) if name == expected_name => {}
57-
Some(name) => {
58-
return Err(PyValueError::new_err(format!(
59-
"Expected capsule named {expected_name:?}, got {name:?}"
60-
)));
61-
}
62-
None => {
63-
return Err(PyValueError::new_err(format!(
64-
"Expected capsule named {expected_name:?}, got unnamed capsule"
65-
)));
66-
}
67-
}
68-
69-
let data = NonNull::new(capsule.pointer().cast::<FFI_LogicalExtensionCodec>())
70-
.ok_or_else(|| PyValueError::new_err("Null logical extension codec capsule pointer"))?;
71-
let codec = unsafe { data.as_ref() };
53+
let ptr = capsule.pointer_checked(Some(expected_name))?;
54+
let codec = unsafe { ptr.cast::<FFI_LogicalExtensionCodec>().as_ref() };
7255

7356
Ok(codec.clone())
7457
}

crates/integration_tests/tests/read_tables.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,7 +1699,7 @@ async fn test_time_travel_by_snapshot_id() {
16991699

17001700
// Snapshot 1: (1, 'alice'), (2, 'bob')
17011701
let table_snap1 = table.copy_with_options(HashMap::from([(
1702-
"scan.snapshot-id".to_string(),
1702+
"scan.version".to_string(),
17031703
"1".to_string(),
17041704
)]));
17051705
let rb = table_snap1.new_read_builder();
@@ -1720,7 +1720,7 @@ async fn test_time_travel_by_snapshot_id() {
17201720

17211721
// Snapshot 2: (1, 'alice'), (2, 'bob'), (3, 'carol'), (4, 'dave')
17221722
let table_snap2 = table.copy_with_options(HashMap::from([(
1723-
"scan.snapshot-id".to_string(),
1723+
"scan.version".to_string(),
17241724
"2".to_string(),
17251725
)]));
17261726
let rb2 = table_snap2.new_read_builder();
@@ -1753,7 +1753,7 @@ async fn test_time_travel_by_tag_name() {
17531753

17541754
// Tag 'snapshot1' -> snapshot 1: (1, 'alice'), (2, 'bob')
17551755
let table_tag1 = table.copy_with_options(HashMap::from([(
1756-
"scan.tag-name".to_string(),
1756+
"scan.version".to_string(),
17571757
"snapshot1".to_string(),
17581758
)]));
17591759
let rb = table_tag1.new_read_builder();
@@ -1774,7 +1774,7 @@ async fn test_time_travel_by_tag_name() {
17741774

17751775
// Tag 'snapshot2' -> snapshot 2: all 4 rows
17761776
let table_tag2 = table.copy_with_options(HashMap::from([(
1777-
"scan.tag-name".to_string(),
1777+
"scan.version".to_string(),
17781778
"snapshot2".to_string(),
17791779
)]));
17801780
let rb2 = table_tag2.new_read_builder();
@@ -1805,8 +1805,8 @@ async fn test_time_travel_conflicting_selectors_fail() {
18051805
let table = get_table_from_catalog(&catalog, "time_travel_table").await;
18061806

18071807
let conflicted = table.copy_with_options(HashMap::from([
1808-
("scan.tag-name".to_string(), "snapshot1".to_string()),
1809-
("scan.snapshot-id".to_string(), "2".to_string()),
1808+
("scan.version".to_string(), "snapshot1".to_string()),
1809+
("scan.timestamp-millis".to_string(), "1234".to_string()),
18101810
]));
18111811

18121812
let plan_err = conflicted
@@ -1823,40 +1823,40 @@ async fn test_time_travel_conflicting_selectors_fail() {
18231823
"unexpected conflict error: {message}"
18241824
);
18251825
assert!(
1826-
message.contains("scan.snapshot-id"),
1827-
"conflict error should mention scan.snapshot-id: {message}"
1826+
message.contains("scan.version"),
1827+
"conflict error should mention scan.version: {message}"
18281828
);
18291829
assert!(
1830-
message.contains("scan.tag-name"),
1831-
"conflict error should mention scan.tag-name: {message}"
1830+
message.contains("scan.timestamp-millis"),
1831+
"conflict error should mention scan.timestamp-millis: {message}"
18321832
);
18331833
}
18341834
other => panic!("unexpected error: {other:?}"),
18351835
}
18361836
}
18371837

18381838
#[tokio::test]
1839-
async fn test_time_travel_invalid_numeric_selector_fails() {
1839+
async fn test_time_travel_invalid_version_fails() {
18401840
let catalog = create_file_system_catalog();
18411841
let table = get_table_from_catalog(&catalog, "time_travel_table").await;
18421842

18431843
let invalid = table.copy_with_options(HashMap::from([(
1844-
"scan.snapshot-id".to_string(),
1845-
"not-a-number".to_string(),
1844+
"scan.version".to_string(),
1845+
"nonexistent-tag".to_string(),
18461846
)]));
18471847

18481848
let plan_err = invalid
18491849
.new_read_builder()
18501850
.new_scan()
18511851
.plan()
18521852
.await
1853-
.expect_err("invalid numeric time-travel selector should fail");
1853+
.expect_err("invalid version should fail");
18541854

18551855
match plan_err {
18561856
Error::DataInvalid { message, .. } => {
18571857
assert!(
1858-
message.contains("Invalid value for scan.snapshot-id"),
1859-
"unexpected invalid selector error: {message}"
1858+
message.contains("is not a valid tag name or snapshot id"),
1859+
"unexpected invalid version error: {message}"
18601860
);
18611861
}
18621862
other => panic!("unexpected error: {other:?}"),

crates/integrations/datafusion/src/ddl.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,14 @@ impl PaimonDdlHandler {
9999

100100
match &statements[0] {
101101
Statement::CreateTable(create_table) => self.handle_create_table(create_table).await,
102-
Statement::AlterTable {
103-
name,
104-
operations,
105-
if_exists,
106-
..
107-
} => self.handle_alter_table(name, operations, *if_exists).await,
102+
Statement::AlterTable(alter_table) => {
103+
self.handle_alter_table(
104+
&alter_table.name,
105+
&alter_table.operations,
106+
alter_table.if_exists,
107+
)
108+
.await
109+
}
108110
_ => self.ctx.sql(sql).await,
109111
}
110112
}
@@ -146,12 +148,12 @@ impl PaimonDdlHandler {
146148

147149
// Primary key from constraints: PRIMARY KEY (col, ...)
148150
for constraint in &ct.constraints {
149-
if let datafusion::sql::sqlparser::ast::TableConstraint::PrimaryKey {
150-
columns, ..
151-
} = constraint
152-
{
153-
let pk_cols: Vec<String> =
154-
columns.iter().map(|c| c.column.expr.to_string()).collect();
151+
if let datafusion::sql::sqlparser::ast::TableConstraint::PrimaryKey(pk) = constraint {
152+
let pk_cols: Vec<String> = pk
153+
.columns
154+
.iter()
155+
.map(|c| c.column.expr.to_string())
156+
.collect();
155157
builder = builder.primary_key(pk_cols);
156158
}
157159
}

crates/integrations/datafusion/src/physical_plan/scan.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub struct PaimonTableScan {
5151
/// Paimon splits that DataFusion partition `i` will read.
5252
/// Wrapped in `Arc` to avoid deep-cloning `DataSplit` metadata in `execute()`.
5353
planned_partitions: Vec<Arc<[DataSplit]>>,
54-
plan_properties: PlanProperties,
54+
plan_properties: Arc<PlanProperties>,
5555
/// Optional limit on the number of rows to return.
5656
limit: Option<usize>,
5757
}
@@ -65,12 +65,12 @@ impl PaimonTableScan {
6565
planned_partitions: Vec<Arc<[DataSplit]>>,
6666
limit: Option<usize>,
6767
) -> Self {
68-
let plan_properties = PlanProperties::new(
68+
let plan_properties = Arc::new(PlanProperties::new(
6969
EquivalenceProperties::new(schema.clone()),
7070
Partitioning::UnknownPartitioning(planned_partitions.len()),
7171
EmissionType::Incremental,
7272
Boundedness::Bounded,
73-
);
73+
));
7474
Self {
7575
table,
7676
projected_columns,
@@ -109,7 +109,7 @@ impl ExecutionPlan for PaimonTableScan {
109109
self
110110
}
111111

112-
fn properties(&self) -> &PlanProperties {
112+
fn properties(&self) -> &Arc<PlanProperties> {
113113
&self.plan_properties
114114
}
115115

@@ -168,10 +168,6 @@ impl ExecutionPlan for PaimonTableScan {
168168
)))
169169
}
170170

171-
fn statistics(&self) -> DFResult<Statistics> {
172-
self.partition_statistics(None)
173-
}
174-
175171
fn partition_statistics(&self, partition: Option<usize>) -> DFResult<Statistics> {
176172
let partitions: &[Arc<[DataSplit]>] = match partition {
177173
Some(idx) => std::slice::from_ref(&self.planned_partitions[idx]),

0 commit comments

Comments
 (0)