From c73c0bb2416b2cbcced8b6a0d9598ae5c6ece4e9 Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Sun, 4 Jan 2026 05:40:07 +0000 Subject: [PATCH 01/11] added truncate table support --- datafusion/catalog/src/table.rs | 8 ++ datafusion/core/src/physical_planner.rs | 25 +++++ .../custom_sources_cases/dml_planning.rs | 100 +++++++++++++++++- datafusion/expr/src/logical_plan/dml.rs | 3 + datafusion/proto/src/generated/prost.rs | 2 + .../proto/src/logical_plan/from_proto.rs | 1 + datafusion/proto/src/logical_plan/to_proto.rs | 1 + datafusion/sql/src/statement.rs | 22 ++++ .../sqllogictest/test_files/dml_truncate.slt | 91 ++++++++++++++++ .../sqllogictest/test_files/truncate.slt | 52 +++++++++ 10 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 datafusion/sqllogictest/test_files/dml_truncate.slt create mode 100644 datafusion/sqllogictest/test_files/truncate.slt diff --git a/datafusion/catalog/src/table.rs b/datafusion/catalog/src/table.rs index 1f223852c2b9d..fbfc530fb0a6d 100644 --- a/datafusion/catalog/src/table.rs +++ b/datafusion/catalog/src/table.rs @@ -353,6 +353,14 @@ pub trait TableProvider: Debug + Sync + Send { ) -> Result> { not_impl_err!("UPDATE not supported for {} table", self.table_type()) } + + /// Truncate rows + async fn truncate( + &self, + _state: &dyn Session, + ) -> Result> { + not_impl_err!("TRUNCATE not supported for {}", self.table_type()) + } } /// Arguments for scanning a table with [`TableProvider::scan_with_args`]. diff --git a/datafusion/core/src/physical_planner.rs b/datafusion/core/src/physical_planner.rs index cc7d534776d7e..4aa0e726c5c67 100644 --- a/datafusion/core/src/physical_planner.rs +++ b/datafusion/core/src/physical_planner.rs @@ -655,6 +655,31 @@ impl DefaultPhysicalPlanner { ); } } + LogicalPlan::Dml(DmlStatement { + table_name, + target, + op: WriteOp::Truncate, + .. + }) => { + if let Some(provider) = + target.as_any().downcast_ref::() + { + provider + .table_provider + .truncate(session_state) + .await + .map_err(|e| { + e.context(format!( + "TRUNCATE operation on table '{}'", + table_name + )) + })? + } else { + return exec_err!( + "Table source can't be downcasted to DefaultTableSource" + ); + } + } LogicalPlan::Window(Window { window_expr, .. }) => { assert_or_internal_err!( !window_expr.is_empty(), diff --git a/datafusion/core/tests/custom_sources_cases/dml_planning.rs b/datafusion/core/tests/custom_sources_cases/dml_planning.rs index 84cf97710a902..15b39ac239e28 100644 --- a/datafusion/core/tests/custom_sources_cases/dml_planning.rs +++ b/datafusion/core/tests/custom_sources_cases/dml_planning.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -//! Tests for DELETE and UPDATE planning to verify filter and assignment extraction. +//! Tests for DELETE, UPDATE, and TRUNCATE planning to verify filter and assignment extraction. use std::any::Any; use std::sync::{Arc, Mutex}; @@ -165,6 +165,69 @@ impl TableProvider for CaptureUpdateProvider { } } +/// A TableProvider that captures whether truncate() was called. +struct CaptureTruncateProvider { + schema: SchemaRef, + truncate_called: Arc>, +} + +impl CaptureTruncateProvider { + fn new(schema: SchemaRef) -> Self { + Self { + schema, + truncate_called: Arc::new(Mutex::new(false)), + } + } + + fn was_truncated(&self) -> bool { + *self.truncate_called.lock().unwrap() + } +} + +impl std::fmt::Debug for CaptureTruncateProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CaptureTruncateProvider") + .field("schema", &self.schema) + .finish() + } +} + +#[async_trait] +impl TableProvider for CaptureTruncateProvider { + fn as_any(&self) -> &dyn Any { + self + } + + fn schema(&self) -> SchemaRef { + Arc::clone(&self.schema) + } + + fn table_type(&self) -> TableType { + TableType::Base + } + + async fn scan( + &self, + _state: &dyn Session, + _projection: Option<&Vec>, + _filters: &[Expr], + _limit: Option, + ) -> Result> { + Ok(Arc::new(EmptyExec::new(Arc::clone(&self.schema)))) + } + + async fn truncate( + &self, + _state: &dyn Session, + ) -> Result> { + *self.truncate_called.lock().unwrap() = true; + + Ok(Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![ + Field::new("count", DataType::UInt64, false), + ]))))) + } +} + fn test_schema() -> SchemaRef { Arc::new(Schema::new(vec![ Field::new("id", DataType::Int32, false), @@ -269,6 +332,26 @@ async fn test_update_assignments() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_truncate_calls_provider() -> Result<()> { + let provider = Arc::new(CaptureTruncateProvider::new(test_schema())); + let ctx = SessionContext::new(); + + ctx.register_table("t", Arc::clone(&provider) as Arc)?; + + ctx.sql("TRUNCATE TABLE t") + .await? + .collect() + .await?; + + assert!( + provider.was_truncated(), + "truncate() should be called on the TableProvider" + ); + + Ok(()) +} + #[tokio::test] async fn test_unsupported_table_delete() -> Result<()> { let schema = test_schema(); @@ -295,3 +378,18 @@ async fn test_unsupported_table_update() -> Result<()> { assert!(result.is_err() || result.unwrap().collect().await.is_err()); Ok(()) } + +#[tokio::test] +async fn test_unsupported_table_truncate() -> Result<()> { + let schema = test_schema(); + let ctx = SessionContext::new(); + + let empty_table = datafusion::datasource::empty::EmptyTable::new(schema); + ctx.register_table("empty_t", Arc::new(empty_table))?; + + let result = ctx.sql("TRUNCATE TABLE empty_t").await; + + assert!(result.is_err() || result.unwrap().collect().await.is_err()); + + Ok(()) +} \ No newline at end of file diff --git a/datafusion/expr/src/logical_plan/dml.rs b/datafusion/expr/src/logical_plan/dml.rs index 6ac3b309aa0c7..b668cbfe2cc35 100644 --- a/datafusion/expr/src/logical_plan/dml.rs +++ b/datafusion/expr/src/logical_plan/dml.rs @@ -237,6 +237,8 @@ pub enum WriteOp { Update, /// `CREATE TABLE AS SELECT` operation Ctas, + /// `TRUNCATE` operation + Truncate, } impl WriteOp { @@ -247,6 +249,7 @@ impl WriteOp { WriteOp::Delete => "Delete", WriteOp::Update => "Update", WriteOp::Ctas => "Ctas", + WriteOp::Truncate => "Truncate", } } } diff --git a/datafusion/proto/src/generated/prost.rs b/datafusion/proto/src/generated/prost.rs index cf343e0258d0b..d232c8f31abef 100644 --- a/datafusion/proto/src/generated/prost.rs +++ b/datafusion/proto/src/generated/prost.rs @@ -444,6 +444,7 @@ pub mod dml_node { InsertAppend = 3, InsertOverwrite = 4, InsertReplace = 5, + Truncate = 6, } impl Type { /// String value of the enum field names used in the ProtoBuf definition. @@ -458,6 +459,7 @@ pub mod dml_node { Self::InsertAppend => "INSERT_APPEND", Self::InsertOverwrite => "INSERT_OVERWRITE", Self::InsertReplace => "INSERT_REPLACE", + Self::Truncate => "TRUNCATE", } } /// Creates an enum from field names used in the ProtoBuf definition. diff --git a/datafusion/proto/src/logical_plan/from_proto.rs b/datafusion/proto/src/logical_plan/from_proto.rs index 179fe8bb7d7fe..a653f517b7275 100644 --- a/datafusion/proto/src/logical_plan/from_proto.rs +++ b/datafusion/proto/src/logical_plan/from_proto.rs @@ -239,6 +239,7 @@ impl From for WriteOp { } protobuf::dml_node::Type::InsertReplace => WriteOp::Insert(InsertOp::Replace), protobuf::dml_node::Type::Ctas => WriteOp::Ctas, + protobuf::dml_node::Type::Truncate => WriteOp::Truncate, } } } diff --git a/datafusion/proto/src/logical_plan/to_proto.rs b/datafusion/proto/src/logical_plan/to_proto.rs index 6e4e5d0b6eea4..b5f2e823cbcc7 100644 --- a/datafusion/proto/src/logical_plan/to_proto.rs +++ b/datafusion/proto/src/logical_plan/to_proto.rs @@ -728,6 +728,7 @@ impl From<&WriteOp> for protobuf::dml_node::Type { WriteOp::Delete => protobuf::dml_node::Type::Delete, WriteOp::Update => protobuf::dml_node::Type::Update, WriteOp::Ctas => protobuf::dml_node::Type::Ctas, + WriteOp::Truncate => protobuf::dml_node::Type::Truncate, } } } diff --git a/datafusion/sql/src/statement.rs b/datafusion/sql/src/statement.rs index 1acbcc92dfe19..f56b053ddba40 100644 --- a/datafusion/sql/src/statement.rs +++ b/datafusion/sql/src/statement.rs @@ -1362,6 +1362,28 @@ impl SqlToRel<'_, S> { exec_err!("Function name not provided") } } + Statement::Truncate { table_names, .. } => { + if table_names.len() != 1 { + return not_impl_err!("TRUNCATE with multiple tables is not supported"); + } + + let target = &table_names[0]; // TruncateTableTarget + let table = self.object_name_to_table_reference(target.name.clone())?; + let source = self.context_provider.get_table_source(table.clone())?; + + Ok(LogicalPlan::Dml(DmlStatement { + table_name: table.clone(), + target: source, + op: WriteOp::Truncate, + input: Arc::new(LogicalPlan::EmptyRelation( + EmptyRelation { + produce_one_row: false, + schema: DFSchemaRef::new(DFSchema::empty()), + }, + )), + output_schema: DFSchemaRef::new(DFSchema::empty()), + })) + } Statement::CreateIndex(CreateIndex { name, table_name, diff --git a/datafusion/sqllogictest/test_files/dml_truncate.slt b/datafusion/sqllogictest/test_files/dml_truncate.slt new file mode 100644 index 0000000000000..93420e4cd891a --- /dev/null +++ b/datafusion/sqllogictest/test_files/dml_truncate.slt @@ -0,0 +1,91 @@ + +# 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. + +########## +## Truncate tests for MemTable +########## + +# Test that TRUNCATE preserves table structure but removes data +statement ok +truncate table t1; + +query I +select count(*) from t1; +---- +0 + +query TT +describe t1; +---- +a:Int32:None +b:Utf8:None +c:Float64:None +d:Int32:None + +# Test TRUNCATE then INSERT works correctly +statement ok +truncate table t1; + +statement ok +insert into t1 values (10, 'new', 1.23, 100); + +query I +select count(*) from t1; +---- +1 + +query I +select a from t1; +---- +10 + +# Test multiple TRUNCATE operations +statement ok +insert into t1 values (20, 'another', 4.56, 200); + +query I +select count(*) from t1; +---- +2 + +statement ok +truncate table t1; + +query I +select count(*) from t1; +---- +0 + +# Truncate already empty table +statement ok +truncate table t1; + +query I +select count(*) from t1; +---- +0 + +# Clean up +statement ok +drop table t1; + +statement ok +drop table test_schema.t5; + +statement ok +drop schema test_schema; \ No newline at end of file diff --git a/datafusion/sqllogictest/test_files/truncate.slt b/datafusion/sqllogictest/test_files/truncate.slt new file mode 100644 index 0000000000000..6a3c8c8b9d0b1 --- /dev/null +++ b/datafusion/sqllogictest/test_files/truncate.slt @@ -0,0 +1,52 @@ +# 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. + +########## +## Truncate Tests +########## + +statement ok +create table t1(a int, b varchar, c double, d int); + +statement ok +insert into t1 values (1, 'abc', 3.14, 4), (2, 'def', 2.71, 5); + +# Truncate all rows from table +query TT +explain truncate table t1; +---- +logical_plan +01)Dml: op=[Truncate] table=[t1] +physical_plan +01)CooperativeExec +02)--DmlResultExec: rows_affected=0 + +# Test TRUNCATE with fully qualified table name +statement ok +create schema test_schema; + +statement ok +create table test_schema.t5(a int); + +query TT +explain truncate table test_schema.t5; +---- +logical_plan +01)Dml: op=[Truncate] table=[test_schema.t5] +physical_plan +01)CooperativeExec +02)--DmlResultExec: rows_affected=0 From 9f44f3907ded19a4ae007e429fbde29a0cc5efb3 Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Sun, 4 Jan 2026 07:08:44 +0000 Subject: [PATCH 02/11] formatting fixed --- datafusion/catalog/src/table.rs | 5 +---- .../tests/custom_sources_cases/dml_planning.rs | 12 +++--------- datafusion/proto/proto/datafusion.proto | 1 + datafusion/proto/src/generated/pbjson.rs | 3 +++ datafusion/proto/src/generated/prost.rs | 1 + datafusion/sql/src/statement.rs | 14 +++++++------- 6 files changed, 16 insertions(+), 20 deletions(-) diff --git a/datafusion/catalog/src/table.rs b/datafusion/catalog/src/table.rs index fbfc530fb0a6d..8da58920f481c 100644 --- a/datafusion/catalog/src/table.rs +++ b/datafusion/catalog/src/table.rs @@ -355,10 +355,7 @@ pub trait TableProvider: Debug + Sync + Send { } /// Truncate rows - async fn truncate( - &self, - _state: &dyn Session, - ) -> Result> { + async fn truncate(&self, _state: &dyn Session) -> Result> { not_impl_err!("TRUNCATE not supported for {}", self.table_type()) } } diff --git a/datafusion/core/tests/custom_sources_cases/dml_planning.rs b/datafusion/core/tests/custom_sources_cases/dml_planning.rs index 15b39ac239e28..ace532b7026d2 100644 --- a/datafusion/core/tests/custom_sources_cases/dml_planning.rs +++ b/datafusion/core/tests/custom_sources_cases/dml_planning.rs @@ -216,10 +216,7 @@ impl TableProvider for CaptureTruncateProvider { Ok(Arc::new(EmptyExec::new(Arc::clone(&self.schema)))) } - async fn truncate( - &self, - _state: &dyn Session, - ) -> Result> { + async fn truncate(&self, _state: &dyn Session) -> Result> { *self.truncate_called.lock().unwrap() = true; Ok(Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![ @@ -339,10 +336,7 @@ async fn test_truncate_calls_provider() -> Result<()> { ctx.register_table("t", Arc::clone(&provider) as Arc)?; - ctx.sql("TRUNCATE TABLE t") - .await? - .collect() - .await?; + ctx.sql("TRUNCATE TABLE t").await?.collect().await?; assert!( provider.was_truncated(), @@ -392,4 +386,4 @@ async fn test_unsupported_table_truncate() -> Result<()> { assert!(result.is_err() || result.unwrap().collect().await.is_err()); Ok(()) -} \ No newline at end of file +} diff --git a/datafusion/proto/proto/datafusion.proto b/datafusion/proto/proto/datafusion.proto index bd7dd3a6aff3c..4b5f349ee1d09 100644 --- a/datafusion/proto/proto/datafusion.proto +++ b/datafusion/proto/proto/datafusion.proto @@ -278,6 +278,7 @@ message DmlNode{ INSERT_APPEND = 3; INSERT_OVERWRITE = 4; INSERT_REPLACE = 5; + TRUNCATE = 6; } Type dml_type = 1; LogicalPlanNode input = 2; diff --git a/datafusion/proto/src/generated/pbjson.rs b/datafusion/proto/src/generated/pbjson.rs index e269606d163a3..de2851b712ef8 100644 --- a/datafusion/proto/src/generated/pbjson.rs +++ b/datafusion/proto/src/generated/pbjson.rs @@ -5236,6 +5236,7 @@ impl serde::Serialize for dml_node::Type { Self::InsertAppend => "INSERT_APPEND", Self::InsertOverwrite => "INSERT_OVERWRITE", Self::InsertReplace => "INSERT_REPLACE", + Self::Truncate => "TRUNCATE", }; serializer.serialize_str(variant) } @@ -5253,6 +5254,7 @@ impl<'de> serde::Deserialize<'de> for dml_node::Type { "INSERT_APPEND", "INSERT_OVERWRITE", "INSERT_REPLACE", + "TRUNCATE", ]; struct GeneratedVisitor; @@ -5299,6 +5301,7 @@ impl<'de> serde::Deserialize<'de> for dml_node::Type { "INSERT_APPEND" => Ok(dml_node::Type::InsertAppend), "INSERT_OVERWRITE" => Ok(dml_node::Type::InsertOverwrite), "INSERT_REPLACE" => Ok(dml_node::Type::InsertReplace), + "TRUNCATE" => Ok(dml_node::Type::Truncate), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } diff --git a/datafusion/proto/src/generated/prost.rs b/datafusion/proto/src/generated/prost.rs index d232c8f31abef..ab88067cc13ef 100644 --- a/datafusion/proto/src/generated/prost.rs +++ b/datafusion/proto/src/generated/prost.rs @@ -471,6 +471,7 @@ pub mod dml_node { "INSERT_APPEND" => Some(Self::InsertAppend), "INSERT_OVERWRITE" => Some(Self::InsertOverwrite), "INSERT_REPLACE" => Some(Self::InsertReplace), + "TRUNCATE" => Some(Self::Truncate), _ => None, } } diff --git a/datafusion/sql/src/statement.rs b/datafusion/sql/src/statement.rs index f56b053ddba40..5806bbd1ee44e 100644 --- a/datafusion/sql/src/statement.rs +++ b/datafusion/sql/src/statement.rs @@ -1364,7 +1364,9 @@ impl SqlToRel<'_, S> { } Statement::Truncate { table_names, .. } => { if table_names.len() != 1 { - return not_impl_err!("TRUNCATE with multiple tables is not supported"); + return not_impl_err!( + "TRUNCATE with multiple tables is not supported" + ); } let target = &table_names[0]; // TruncateTableTarget @@ -1375,12 +1377,10 @@ impl SqlToRel<'_, S> { table_name: table.clone(), target: source, op: WriteOp::Truncate, - input: Arc::new(LogicalPlan::EmptyRelation( - EmptyRelation { - produce_one_row: false, - schema: DFSchemaRef::new(DFSchema::empty()), - }, - )), + input: Arc::new(LogicalPlan::EmptyRelation(EmptyRelation { + produce_one_row: false, + schema: DFSchemaRef::new(DFSchema::empty()), + })), output_schema: DFSchemaRef::new(DFSchema::empty()), })) } From aad03789273d3704cffbc1462e96b5abf7e48183 Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Sun, 4 Jan 2026 07:41:04 +0000 Subject: [PATCH 03/11] test and clippy issue fixed --- datafusion/core/src/physical_planner.rs | 5 +- .../sqllogictest/test_files/dml_truncate.slt | 91 ------------------- .../sqllogictest/test_files/truncate.slt | 16 ++-- 3 files changed, 11 insertions(+), 101 deletions(-) delete mode 100644 datafusion/sqllogictest/test_files/dml_truncate.slt diff --git a/datafusion/core/src/physical_planner.rs b/datafusion/core/src/physical_planner.rs index 4aa0e726c5c67..168efe286daf6 100644 --- a/datafusion/core/src/physical_planner.rs +++ b/datafusion/core/src/physical_planner.rs @@ -669,10 +669,7 @@ impl DefaultPhysicalPlanner { .truncate(session_state) .await .map_err(|e| { - e.context(format!( - "TRUNCATE operation on table '{}'", - table_name - )) + e.context(format!("TRUNCATE operation on table '{}'", table_name)) })? } else { return exec_err!( diff --git a/datafusion/sqllogictest/test_files/dml_truncate.slt b/datafusion/sqllogictest/test_files/dml_truncate.slt deleted file mode 100644 index 93420e4cd891a..0000000000000 --- a/datafusion/sqllogictest/test_files/dml_truncate.slt +++ /dev/null @@ -1,91 +0,0 @@ - -# 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. - -########## -## Truncate tests for MemTable -########## - -# Test that TRUNCATE preserves table structure but removes data -statement ok -truncate table t1; - -query I -select count(*) from t1; ----- -0 - -query TT -describe t1; ----- -a:Int32:None -b:Utf8:None -c:Float64:None -d:Int32:None - -# Test TRUNCATE then INSERT works correctly -statement ok -truncate table t1; - -statement ok -insert into t1 values (10, 'new', 1.23, 100); - -query I -select count(*) from t1; ----- -1 - -query I -select a from t1; ----- -10 - -# Test multiple TRUNCATE operations -statement ok -insert into t1 values (20, 'another', 4.56, 200); - -query I -select count(*) from t1; ----- -2 - -statement ok -truncate table t1; - -query I -select count(*) from t1; ----- -0 - -# Truncate already empty table -statement ok -truncate table t1; - -query I -select count(*) from t1; ----- -0 - -# Clean up -statement ok -drop table t1; - -statement ok -drop table test_schema.t5; - -statement ok -drop schema test_schema; \ No newline at end of file diff --git a/datafusion/sqllogictest/test_files/truncate.slt b/datafusion/sqllogictest/test_files/truncate.slt index 6a3c8c8b9d0b1..1fc7ecfc42df4 100644 --- a/datafusion/sqllogictest/test_files/truncate.slt +++ b/datafusion/sqllogictest/test_files/truncate.slt @@ -31,9 +31,11 @@ explain truncate table t1; ---- logical_plan 01)Dml: op=[Truncate] table=[t1] -physical_plan -01)CooperativeExec -02)--DmlResultExec: rows_affected=0 +02)--EmptyRelation: rows=0 +physical_plan_error +01)TRUNCATE operation on table 't1' +02)caused by +03)This feature is not implemented: TRUNCATE not supported for Base # Test TRUNCATE with fully qualified table name statement ok @@ -47,6 +49,8 @@ explain truncate table test_schema.t5; ---- logical_plan 01)Dml: op=[Truncate] table=[test_schema.t5] -physical_plan -01)CooperativeExec -02)--DmlResultExec: rows_affected=0 +02)--EmptyRelation: rows=0 +physical_plan_error +01)TRUNCATE operation on table 'test_schema.t5' +02)caused by +03)This feature is not implemented: TRUNCATE not supported for Base From 4a1fb16bed55c91b51f1f8c6629146799c484376 Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Sun, 4 Jan 2026 07:45:53 +0000 Subject: [PATCH 04/11] format resolved --- datafusion/core/src/physical_planner.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/datafusion/core/src/physical_planner.rs b/datafusion/core/src/physical_planner.rs index 168efe286daf6..7ca6636d6ac47 100644 --- a/datafusion/core/src/physical_planner.rs +++ b/datafusion/core/src/physical_planner.rs @@ -669,7 +669,9 @@ impl DefaultPhysicalPlanner { .truncate(session_state) .await .map_err(|e| { - e.context(format!("TRUNCATE operation on table '{}'", table_name)) + e.context(format!( + "TRUNCATE operation on table '{table_name}'" + )) })? } else { return exec_err!( From a6196a3fb7baf28a7d94753b962b9d0c84fb2792 Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Sun, 4 Jan 2026 10:32:42 +0000 Subject: [PATCH 05/11] comment improved --- datafusion/catalog/src/table.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/datafusion/catalog/src/table.rs b/datafusion/catalog/src/table.rs index 8da58920f481c..3bf493fc5a024 100644 --- a/datafusion/catalog/src/table.rs +++ b/datafusion/catalog/src/table.rs @@ -354,7 +354,10 @@ pub trait TableProvider: Debug + Sync + Send { not_impl_err!("UPDATE not supported for {} table", self.table_type()) } - /// Truncate rows + /// Remove all rows from the table. + /// + /// Returns an [`ExecutionPlan`] producing a single row with `count` (UInt64), + /// representing the number of rows removed. async fn truncate(&self, _state: &dyn Session) -> Result> { not_impl_err!("TRUNCATE not supported for {}", self.table_type()) } From 12e59205534f143e7b6fd28bf4849372533c00e4 Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Mon, 5 Jan 2026 11:25:54 +0300 Subject: [PATCH 06/11] chore : reject unsupported options --- datafusion/catalog/src/table.rs | 2 +- datafusion/sql/src/statement.rs | 43 +++++++++++++++---- .../sqllogictest/test_files/truncate.slt | 12 +++++- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/datafusion/catalog/src/table.rs b/datafusion/catalog/src/table.rs index 3bf493fc5a024..c10c0e26203bc 100644 --- a/datafusion/catalog/src/table.rs +++ b/datafusion/catalog/src/table.rs @@ -359,7 +359,7 @@ pub trait TableProvider: Debug + Sync + Send { /// Returns an [`ExecutionPlan`] producing a single row with `count` (UInt64), /// representing the number of rows removed. async fn truncate(&self, _state: &dyn Session) -> Result> { - not_impl_err!("TRUNCATE not supported for {}", self.table_type()) + not_impl_err!("TRUNCATE not supported for {} table", self.table_type()) } } diff --git a/datafusion/sql/src/statement.rs b/datafusion/sql/src/statement.rs index 5806bbd1ee44e..2c4a52a275797 100644 --- a/datafusion/sql/src/statement.rs +++ b/datafusion/sql/src/statement.rs @@ -1362,27 +1362,52 @@ impl SqlToRel<'_, S> { exec_err!("Function name not provided") } } - Statement::Truncate { table_names, .. } => { + Statement::Truncate { + table_names, + partitions, + identity, + cascade, + on_cluster, + .. + } => { if table_names.len() != 1 { return not_impl_err!( "TRUNCATE with multiple tables is not supported" ); } - let target = &table_names[0]; // TruncateTableTarget + let target = &table_names[0]; + if target.only { + return not_impl_err!("TRUNCATE with ONLY is not supported"); + } + if partitions.is_some() { + return not_impl_err!("TRUNCATE with PARTITION is not supported"); + } + if identity.is_some() { + return not_impl_err!( + "TRUNCATE with RESTART/CONTINUE IDENTITY is not supported" + ); + } + if cascade.is_some() { + return not_impl_err!( + "TRUNCATE with CASCADE/RESTRICT is not supported" + ); + } + if on_cluster.is_some() { + return not_impl_err!("TRUNCATE with ON CLUSTER is not supported"); + } let table = self.object_name_to_table_reference(target.name.clone())?; let source = self.context_provider.get_table_source(table.clone())?; - Ok(LogicalPlan::Dml(DmlStatement { - table_name: table.clone(), - target: source, - op: WriteOp::Truncate, - input: Arc::new(LogicalPlan::EmptyRelation(EmptyRelation { + Ok(LogicalPlan::Dml(DmlStatement::new( + table.clone(), + source, + WriteOp::Truncate, + Arc::new(LogicalPlan::EmptyRelation(EmptyRelation { produce_one_row: false, schema: DFSchemaRef::new(DFSchema::empty()), })), - output_schema: DFSchemaRef::new(DFSchema::empty()), - })) + ))) } Statement::CreateIndex(CreateIndex { name, diff --git a/datafusion/sqllogictest/test_files/truncate.slt b/datafusion/sqllogictest/test_files/truncate.slt index 1fc7ecfc42df4..55c59e77d9d41 100644 --- a/datafusion/sqllogictest/test_files/truncate.slt +++ b/datafusion/sqllogictest/test_files/truncate.slt @@ -35,7 +35,7 @@ logical_plan physical_plan_error 01)TRUNCATE operation on table 't1' 02)caused by -03)This feature is not implemented: TRUNCATE not supported for Base +03)This feature is not implemented: TRUNCATE not supported for Base table # Test TRUNCATE with fully qualified table name statement ok @@ -53,4 +53,12 @@ logical_plan physical_plan_error 01)TRUNCATE operation on table 'test_schema.t5' 02)caused by -03)This feature is not implemented: TRUNCATE not supported for Base +03)This feature is not implemented: TRUNCATE not supported for Base table + +# Test TRUNCATE with CASCADE option +statement error TRUNCATE with CASCADE/RESTRICT is not supported +TRUNCATE TABLE t1 CASCADE; + +# Test TRUNCATE with multiple tables +statement error TRUNCATE with multiple tables is not supported +TRUNCATE TABLE t1, t2; \ No newline at end of file From e59cb23a976df6eedcc56b7bcc95c77d64481cca Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Tue, 6 Jan 2026 08:59:59 +0300 Subject: [PATCH 07/11] added negative tests --- .../custom_sources_cases/dml_planning.rs | 20 ++++++++++++++++--- .../tests/cases/roundtrip_logical_plan.rs | 1 + .../sqllogictest/test_files/truncate.slt | 11 +++++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/datafusion/core/tests/custom_sources_cases/dml_planning.rs b/datafusion/core/tests/custom_sources_cases/dml_planning.rs index ace532b7026d2..0491ba49e784f 100644 --- a/datafusion/core/tests/custom_sources_cases/dml_planning.rs +++ b/datafusion/core/tests/custom_sources_cases/dml_planning.rs @@ -20,7 +20,9 @@ use std::any::Any; use std::sync::{Arc, Mutex}; +use arrow::array::UInt64Array; use arrow::datatypes::{DataType, Field, Schema, SchemaRef}; +use arrow::record_batch::RecordBatch; use async_trait::async_trait; use datafusion::datasource::{TableProvider, TableType}; use datafusion::error::Result; @@ -29,6 +31,7 @@ use datafusion::logical_expr::Expr; use datafusion_catalog::Session; use datafusion_physical_plan::ExecutionPlan; use datafusion_physical_plan::empty::EmptyExec; +use datafusion_physical_plan::test::TestMemoryExec; /// A TableProvider that captures the filters passed to delete_from(). struct CaptureDeleteProvider { @@ -219,9 +222,20 @@ impl TableProvider for CaptureTruncateProvider { async fn truncate(&self, _state: &dyn Session) -> Result> { *self.truncate_called.lock().unwrap() = true; - Ok(Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![ - Field::new("count", DataType::UInt64, false), - ]))))) + let schema = Arc::new(Schema::new(vec![Field::new( + "count", + DataType::UInt64, + false, + )])); + let batch = RecordBatch::try_new( + Arc::clone(&schema), + vec![Arc::new(UInt64Array::from(vec![0u64]))], + )?; + Ok(Arc::new(TestMemoryExec::try_new( + &[vec![batch]], + schema, + None, + )?)) } } diff --git a/datafusion/proto/tests/cases/roundtrip_logical_plan.rs b/datafusion/proto/tests/cases/roundtrip_logical_plan.rs index bcfda648b53e5..ae84d385cf321 100644 --- a/datafusion/proto/tests/cases/roundtrip_logical_plan.rs +++ b/datafusion/proto/tests/cases/roundtrip_logical_plan.rs @@ -413,6 +413,7 @@ async fn roundtrip_logical_plan_dml() -> Result<()> { "DELETE FROM T1", "UPDATE T1 SET a = 1", "CREATE TABLE T2 AS SELECT * FROM T1", + "TRUNCATE TABLE T1", ]; for query in queries { let plan = ctx.sql(query).await?.into_optimized_plan()?; diff --git a/datafusion/sqllogictest/test_files/truncate.slt b/datafusion/sqllogictest/test_files/truncate.slt index 55c59e77d9d41..a33c74ace5be3 100644 --- a/datafusion/sqllogictest/test_files/truncate.slt +++ b/datafusion/sqllogictest/test_files/truncate.slt @@ -61,4 +61,13 @@ TRUNCATE TABLE t1 CASCADE; # Test TRUNCATE with multiple tables statement error TRUNCATE with multiple tables is not supported -TRUNCATE TABLE t1, t2; \ No newline at end of file +TRUNCATE TABLE t1, t2; + +statement error TRUNCATE with PARTITION is not supported +TRUNCATE TABLE t1 PARTITION (p1); + +statement error TRUNCATE with ONLY is not supported +TRUNCATE ONLY t1; + +statement error TRUNCATE with RESTART/CONTINUE IDENTITY is not supported +TRUNCATE TABLE t1 RESTART IDENTITY; \ No newline at end of file From 32075e01ad2867ee02130c8b69307340c07f9c19 Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Tue, 6 Jan 2026 09:37:32 +0300 Subject: [PATCH 08/11] disable the optimizer --- datafusion/core/tests/custom_sources_cases/dml_planning.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/datafusion/core/tests/custom_sources_cases/dml_planning.rs b/datafusion/core/tests/custom_sources_cases/dml_planning.rs index 0491ba49e784f..13e564d73b774 100644 --- a/datafusion/core/tests/custom_sources_cases/dml_planning.rs +++ b/datafusion/core/tests/custom_sources_cases/dml_planning.rs @@ -347,6 +347,10 @@ async fn test_update_assignments() -> Result<()> { async fn test_truncate_calls_provider() -> Result<()> { let provider = Arc::new(CaptureTruncateProvider::new(test_schema())); let ctx = SessionContext::new(); + ctx.state() + .write() + .config_mut() + .set("datafusion.optimizer.max_passes", "0"); ctx.register_table("t", Arc::clone(&provider) as Arc)?; From 760ed221b28fb4a345b8b850492f9e1625f272d2 Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Tue, 6 Jan 2026 07:08:43 +0000 Subject: [PATCH 09/11] test fixed --- .../core/tests/custom_sources_cases/dml_planning.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/datafusion/core/tests/custom_sources_cases/dml_planning.rs b/datafusion/core/tests/custom_sources_cases/dml_planning.rs index 13e564d73b774..f043b41700672 100644 --- a/datafusion/core/tests/custom_sources_cases/dml_planning.rs +++ b/datafusion/core/tests/custom_sources_cases/dml_planning.rs @@ -26,7 +26,7 @@ use arrow::record_batch::RecordBatch; use async_trait::async_trait; use datafusion::datasource::{TableProvider, TableType}; use datafusion::error::Result; -use datafusion::execution::context::SessionContext; +use datafusion::execution::context::{SessionConfig, SessionContext}; use datafusion::logical_expr::Expr; use datafusion_catalog::Session; use datafusion_physical_plan::ExecutionPlan; @@ -346,11 +346,9 @@ async fn test_update_assignments() -> Result<()> { #[tokio::test] async fn test_truncate_calls_provider() -> Result<()> { let provider = Arc::new(CaptureTruncateProvider::new(test_schema())); - let ctx = SessionContext::new(); - ctx.state() - .write() - .config_mut() - .set("datafusion.optimizer.max_passes", "0"); + let config = SessionConfig::new().set("datafusion.optimizer.max_passes", "0"); + + let ctx = SessionContext::new_with_config(config); ctx.register_table("t", Arc::clone(&provider) as Arc)?; From 7a9b4b18df2be44a11e4afa92c9ace4d0d4b767d Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Tue, 6 Jan 2026 07:16:56 +0000 Subject: [PATCH 10/11] type matching fixed --- datafusion/core/tests/custom_sources_cases/dml_planning.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/datafusion/core/tests/custom_sources_cases/dml_planning.rs b/datafusion/core/tests/custom_sources_cases/dml_planning.rs index f043b41700672..6de2f7b4d5f73 100644 --- a/datafusion/core/tests/custom_sources_cases/dml_planning.rs +++ b/datafusion/core/tests/custom_sources_cases/dml_planning.rs @@ -29,6 +29,7 @@ use datafusion::error::Result; use datafusion::execution::context::{SessionConfig, SessionContext}; use datafusion::logical_expr::Expr; use datafusion_catalog::Session; +use datafusion_common::ScalarValue; use datafusion_physical_plan::ExecutionPlan; use datafusion_physical_plan::empty::EmptyExec; use datafusion_physical_plan::test::TestMemoryExec; @@ -346,7 +347,10 @@ async fn test_update_assignments() -> Result<()> { #[tokio::test] async fn test_truncate_calls_provider() -> Result<()> { let provider = Arc::new(CaptureTruncateProvider::new(test_schema())); - let config = SessionConfig::new().set("datafusion.optimizer.max_passes", "0"); + let config = SessionConfig::new().set( + "datafusion.optimizer.max_passes", + &ScalarValue::UInt64(Some(0)), + ); let ctx = SessionContext::new_with_config(config); From e7dc8563df47bc42f767b18f15b3734d9680be4b Mon Sep 17 00:00:00 2001 From: Nachiket Roy Date: Tue, 6 Jan 2026 16:14:45 +0000 Subject: [PATCH 11/11] revert to EmptyExec --- .../custom_sources_cases/dml_planning.rs | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/datafusion/core/tests/custom_sources_cases/dml_planning.rs b/datafusion/core/tests/custom_sources_cases/dml_planning.rs index 6de2f7b4d5f73..a4033e445c213 100644 --- a/datafusion/core/tests/custom_sources_cases/dml_planning.rs +++ b/datafusion/core/tests/custom_sources_cases/dml_planning.rs @@ -20,9 +20,7 @@ use std::any::Any; use std::sync::{Arc, Mutex}; -use arrow::array::UInt64Array; use arrow::datatypes::{DataType, Field, Schema, SchemaRef}; -use arrow::record_batch::RecordBatch; use async_trait::async_trait; use datafusion::datasource::{TableProvider, TableType}; use datafusion::error::Result; @@ -32,7 +30,6 @@ use datafusion_catalog::Session; use datafusion_common::ScalarValue; use datafusion_physical_plan::ExecutionPlan; use datafusion_physical_plan::empty::EmptyExec; -use datafusion_physical_plan::test::TestMemoryExec; /// A TableProvider that captures the filters passed to delete_from(). struct CaptureDeleteProvider { @@ -223,20 +220,9 @@ impl TableProvider for CaptureTruncateProvider { async fn truncate(&self, _state: &dyn Session) -> Result> { *self.truncate_called.lock().unwrap() = true; - let schema = Arc::new(Schema::new(vec![Field::new( - "count", - DataType::UInt64, - false, - )])); - let batch = RecordBatch::try_new( - Arc::clone(&schema), - vec![Arc::new(UInt64Array::from(vec![0u64]))], - )?; - Ok(Arc::new(TestMemoryExec::try_new( - &[vec![batch]], - schema, - None, - )?)) + Ok(Arc::new(EmptyExec::new(Arc::new(Schema::new(vec![ + Field::new("count", DataType::UInt64, false), + ]))))) } }