1use std::collections::HashMap;
37use std::sync::Arc;
38
39use datafusion::arrow::array::{
40 new_null_array, ArrayRef, BooleanArray, Date32Array, Float32Array, Float64Array, Int16Array,
41 Int32Array, Int64Array, Int8Array, StringArray,
42};
43use datafusion::arrow::compute::cast;
44use datafusion::arrow::datatypes::{DataType as ArrowDataType, Field, Schema};
45use datafusion::arrow::record_batch::RecordBatch;
46use datafusion::common::TableReference;
47use datafusion::datasource::{MemTable, TableProvider};
48use datafusion::error::{DataFusionError, Result as DFResult};
49use datafusion::prelude::{DataFrame, SessionConfig, SessionContext};
50use datafusion::sql::sqlparser::ast::{
51 AlterTableOperation, ColumnDef, CreateTable, CreateTableOptions, CreateView, Delete,
52 Expr as SqlExpr, FromTable, Insert, Merge, ObjectName, ObjectType, RenameTableNameKind, Reset,
53 ResetStatement, Set, SqlOption, Statement, TableFactor, TableObject, Truncate, Update,
54 Value as SqlValue,
55};
56use datafusion::sql::sqlparser::dialect::GenericDialect;
57use datafusion::sql::sqlparser::parser::Parser;
58use futures::StreamExt;
59use paimon::catalog::{Catalog, Identifier};
60use paimon::spec::{
61 ArrayType as PaimonArrayType, BigIntType, BlobType, BooleanType, DataField as PaimonDataField,
62 DataType as PaimonDataType, DateType, Datum, DecimalType, DoubleType, FloatType, IntType,
63 LocalZonedTimestampType, MapType as PaimonMapType, RowType as PaimonRowType, SchemaChange,
64 SmallIntType, TimestampType, TinyIntType, VarBinaryType, VarCharType,
65};
66
67use crate::error::to_datafusion_error;
68use crate::DynamicOptions;
69
70pub struct SQLContext {
80 ctx: SessionContext,
81 catalogs: HashMap<String, Arc<dyn Catalog>>,
82 dynamic_options: DynamicOptions,
84}
85
86impl Default for SQLContext {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl SQLContext {
93 pub fn new() -> Self {
95 let ctx =
96 SessionContext::new_with_config(SessionConfig::new().with_information_schema(true));
97 ctx.register_relation_planner(Arc::new(
98 crate::relation_planner::PaimonRelationPlanner::new(),
99 ))
100 .expect("failed to register relation planner");
101 Self {
102 ctx,
103 catalogs: HashMap::new(),
104 dynamic_options: Default::default(),
105 }
106 }
107
108 pub async fn register_catalog(
115 &mut self,
116 catalog_name: impl Into<String>,
117 catalog: Arc<dyn Catalog>,
118 ) -> DFResult<()> {
119 let catalog_name = catalog_name.into();
120 let is_first = self.catalogs.is_empty();
121 let default_db = "default";
122 match catalog.get_database(default_db).await {
123 Ok(_) => {}
124 Err(paimon::Error::DatabaseNotExist { .. }) => {
125 catalog
126 .create_database(default_db, true, Default::default())
127 .await
128 .map_err(|e| DataFusionError::External(Box::new(e)))?;
129 }
130 Err(e) => return Err(DataFusionError::External(Box::new(e))),
131 }
132 self.ctx.register_catalog(
133 &catalog_name,
134 Arc::new(crate::catalog::PaimonCatalogProvider::with_dynamic_options(
135 catalog.clone(),
136 self.dynamic_options.clone(),
137 )),
138 );
139 register_table_functions(&self.ctx, &catalog, default_db);
140 self.catalogs.insert(catalog_name.clone(), catalog);
141 if is_first {
142 self.set_current_catalog(catalog_name).await?;
143 self.set_current_database(default_db).await?;
144 }
145 Ok(())
146 }
147
148 pub async fn set_current_catalog(&mut self, catalog_name: impl Into<String>) -> DFResult<()> {
150 let catalog_name = catalog_name.into();
151 if !self.catalogs.contains_key(&catalog_name) {
152 return Err(DataFusionError::Plan(format!(
153 "Unknown catalog '{catalog_name}'"
154 )));
155 }
156 if catalog_name.contains('\'') {
157 return Err(DataFusionError::Plan(
158 "Catalog name must not contain single quotes".to_string(),
159 ));
160 }
161 self.ctx
162 .sql(&format!(
163 "SET datafusion.catalog.default_catalog = '{catalog_name}'"
164 ))
165 .await?;
166 Ok(())
167 }
168
169 pub async fn set_current_database(&self, database_name: &str) -> DFResult<()> {
171 if database_name.contains('\'') {
172 return Err(DataFusionError::Plan(
173 "Database name must not contain single quotes".to_string(),
174 ));
175 }
176 self.ctx
177 .sql(&format!(
178 "SET datafusion.catalog.default_schema = '{database_name}'"
179 ))
180 .await?;
181 Ok(())
182 }
183
184 pub fn ctx(&self) -> &SessionContext {
186 &self.ctx
187 }
188
189 pub fn register_temp_table(
198 &self,
199 name: impl Into<TableReference>,
200 table: Arc<dyn TableProvider>,
201 ) -> DFResult<()> {
202 let (catalog, database, table_name) = self.resolve_temp_table_name(name.into())?;
203 let catalog_provider = self
204 .ctx
205 .catalog(&catalog)
206 .ok_or_else(|| DataFusionError::Plan(format!("Unknown catalog '{catalog}'")))?;
207
208 let paimon_provider = catalog_provider
209 .as_any()
210 .downcast_ref::<crate::catalog::PaimonCatalogProvider>()
211 .ok_or_else(|| {
212 DataFusionError::Plan(format!("Catalog '{catalog}' is not a Paimon catalog"))
213 })?;
214
215 paimon_provider.register_temp_table(&database, &table_name, table)
216 }
217
218 pub fn deregister_temp_table(
222 &self,
223 name: impl Into<TableReference>,
224 ) -> DFResult<Option<Arc<dyn TableProvider>>> {
225 let (catalog, database, table_name) = self.resolve_temp_table_name(name.into())?;
226 let catalog_provider = self
227 .ctx
228 .catalog(&catalog)
229 .ok_or_else(|| DataFusionError::Plan(format!("Unknown catalog '{catalog}'")))?;
230
231 let paimon_provider = catalog_provider
232 .as_any()
233 .downcast_ref::<crate::catalog::PaimonCatalogProvider>()
234 .ok_or_else(|| {
235 DataFusionError::Plan(format!("Catalog '{catalog}' is not a Paimon catalog"))
236 })?;
237
238 paimon_provider.deregister_temp_table(&database, &table_name)
239 }
240
241 pub fn temp_table_exist(&self, name: impl Into<TableReference>) -> DFResult<bool> {
245 let (catalog, database, table_name) = self.resolve_temp_table_name(name.into())?;
246 let catalog_provider = self
247 .ctx
248 .catalog(&catalog)
249 .ok_or_else(|| DataFusionError::Plan(format!("Unknown catalog '{catalog}'")))?;
250
251 let paimon_provider = catalog_provider
252 .as_any()
253 .downcast_ref::<crate::catalog::PaimonCatalogProvider>()
254 .ok_or_else(|| {
255 DataFusionError::Plan(format!("Catalog '{catalog}' is not a Paimon catalog"))
256 })?;
257
258 Ok(paimon_provider.temp_table_exist(&database, &table_name))
259 }
260
261 fn resolve_temp_table_name(&self, name: TableReference) -> DFResult<(String, String, String)> {
263 match name {
264 TableReference::Bare { table } => {
265 let catalog = self.current_catalog_name();
266 let database = self
267 .ctx
268 .state()
269 .config_options()
270 .catalog
271 .default_schema
272 .clone();
273 Ok((catalog, database, table.to_string()))
274 }
275 TableReference::Partial { schema, table } => {
276 let catalog = self.current_catalog_name();
277 Ok((catalog, schema.to_string(), table.to_string()))
278 }
279 TableReference::Full {
280 catalog,
281 schema,
282 table,
283 } => Ok((catalog.to_string(), schema.to_string(), table.to_string())),
284 }
285 }
286
287 #[cfg(test)]
288 pub(crate) fn dynamic_options(&self) -> &DynamicOptions {
289 &self.dynamic_options
290 }
291
292 pub async fn sql(&self, sql: &str) -> DFResult<DataFrame> {
295 let is_create_table = looks_like_create_table(sql);
296 let (rewritten_sql, partition_keys) = if is_create_table {
297 extract_partition_by(sql)?
298 } else {
299 (sql.to_string(), vec![])
300 };
301 if contains_time_travel_keyword(&rewritten_sql) {
302 return self.handle_time_travel_query(&rewritten_sql).await;
304 }
305
306 let statements = Parser::parse_sql(&GenericDialect {}, &rewritten_sql)
307 .map_err(|e| DataFusionError::Plan(format!("SQL parse error: {e}")))?;
308
309 if statements.len() != 1 {
310 return Err(DataFusionError::Plan(
311 "Expected exactly one SQL statement".to_string(),
312 ));
313 }
314
315 match &statements[0] {
316 Statement::CreateTable(create_table) => {
317 if create_table.temporary {
318 self.handle_create_temp_table(create_table).await
319 } else {
320 let (catalog, _catalog_name, _) =
321 self.resolve_catalog_and_table(&create_table.name)?;
322 self.handle_create_table(&catalog, create_table, partition_keys)
323 .await
324 }
325 }
326 Statement::AlterTable(alter_table) => {
327 let (catalog, _catalog_name, _) =
328 self.resolve_catalog_and_table(&alter_table.name)?;
329 self.handle_alter_table(
330 &catalog,
331 &alter_table.name,
332 &alter_table.operations,
333 alter_table.if_exists,
334 )
335 .await
336 }
337 Statement::Merge(merge) => self.handle_merge_into(merge).await,
338 Statement::Update(update) => self.handle_update(update).await,
339 Statement::Delete(delete) => self.handle_delete(delete).await,
340 Statement::Insert(insert)
341 if insert.overwrite
342 && insert.partitioned.as_ref().is_some_and(|p| !p.is_empty()) =>
343 {
344 self.handle_insert_overwrite_partition(insert).await
345 }
346 Statement::Set(Set::SingleAssignment {
347 variable, values, ..
348 }) => {
349 let key = variable.to_string();
350 let key = key.trim_matches('\'').trim_matches('"');
351 if let Some(paimon_key) = key.strip_prefix("paimon.") {
352 let value = values
353 .first()
354 .ok_or_else(|| DataFusionError::Plan("SET requires a value".to_string()))?
355 .to_string();
356 let value = value
357 .strip_prefix('\'')
358 .and_then(|s| s.strip_suffix('\''))
359 .unwrap_or(&value)
360 .to_string();
361 self.dynamic_options
362 .write()
363 .unwrap()
364 .insert(paimon_key.to_string(), value);
365 return ok_result(&self.ctx);
366 }
367 self.ctx.sql(sql).await
368 }
369 Statement::Reset(ResetStatement {
370 reset: Reset::ConfigurationParameter(name),
371 }) => {
372 let key = name.to_string();
373 let key = key.trim_matches('\'').trim_matches('"');
374 if let Some(paimon_key) = key.strip_prefix("paimon.") {
375 self.dynamic_options.write().unwrap().remove(paimon_key);
376 return ok_result(&self.ctx);
377 }
378 self.ctx.sql(sql).await
379 }
380 Statement::Truncate(truncate) => self.handle_truncate_table(truncate).await,
381 Statement::CreateView(create_view) => {
382 if create_view.temporary {
383 self.handle_create_view(create_view).await
385 } else {
386 let view_name = create_view.name.to_string();
388 let table_ref: TableReference = view_name.as_str().into();
389 if self.is_paimon_catalog_ref(&table_ref) {
390 self.handle_create_view(create_view).await
391 } else {
392 self.ctx.sql(sql).await
393 }
394 }
395 }
396 Statement::Drop {
397 object_type,
398 if_exists,
399 names,
400 temporary,
401 ..
402 } if matches!(*object_type, ObjectType::Table | ObjectType::View) => {
403 if *temporary {
404 self.handle_drop_temp_table(names, *if_exists)
405 } else if *object_type == ObjectType::Table {
406 let table_ref: TableReference = names[0].to_string().as_str().into();
408 if self.is_paimon_catalog_ref(&table_ref) {
409 let (catalog, _catalog_name, _) =
410 self.resolve_catalog_and_table(&names[0])?;
411 self.handle_drop_table(&catalog, names, *if_exists).await
412 } else {
413 self.ctx.sql(sql).await
414 }
415 } else {
416 self.ctx.sql(sql).await
417 }
418 }
419 Statement::Call(func) => {
420 crate::procedures::execute_call(
421 &self.ctx,
422 &self.catalogs,
423 &self.current_catalog_name(),
424 func,
425 )
426 .await
427 }
428 _ => self.ctx.sql(sql).await,
429 }
430 }
431
432 async fn handle_time_travel_query(&self, sql: &str) -> DFResult<DataFrame> {
441 use crate::table::PaimonTableProvider;
442 use paimon::spec::{SCAN_TIMESTAMP_MILLIS_OPTION, SCAN_VERSION_OPTION};
443
444 let mut tracker = crate::merge_into::TempTableTracker::new(self);
445
446 let version_clauses = extract_all_version_as_of(sql);
447 let timestamp_clauses = extract_all_timestamp_as_of(sql);
448
449 if version_clauses.is_empty() && timestamp_clauses.is_empty() {
450 return Err(DataFusionError::Plan(
451 "Failed to parse time-travel clause in SQL".to_string(),
452 ));
453 }
454
455 let mut replacements: Vec<((usize, usize), String)> = Vec::new();
457
458 for info in &version_clauses {
460 let table_ref: datafusion::common::TableReference = info.table_name.as_str().into();
461 let (catalog, _catalog_name, identifier) =
462 self.resolve_table_name_from_ref(&table_ref)?;
463
464 let paimon_table = catalog
465 .get_table(&identifier)
466 .await
467 .map_err(|e| DataFusionError::External(Box::new(e)))?;
468
469 let mut options = self.dynamic_options.read().unwrap().clone();
471 options.insert(SCAN_VERSION_OPTION.to_string(), info.version.clone());
472
473 let table_with_options = paimon_table.copy_with_options(options);
474 let provider = Arc::new(PaimonTableProvider::try_new(table_with_options)?);
475
476 let uuid_name = format!("__paimon_tt_{}", uuid::Uuid::new_v4().as_simple());
477 self.register_temp_table(uuid_name.as_str(), provider)?;
478 tracker.register(&uuid_name);
479 replacements.push((info.clause_range, uuid_name));
480 }
481
482 for info in ×tamp_clauses {
484 let table_ref: datafusion::common::TableReference = info.table_name.as_str().into();
485 let (catalog, _catalog_name, identifier) =
486 self.resolve_table_name_from_ref(&table_ref)?;
487
488 let paimon_table = catalog
489 .get_table(&identifier)
490 .await
491 .map_err(|e| DataFusionError::External(Box::new(e)))?;
492
493 let millis = Self::parse_timestamp_to_millis(&info.timestamp)?;
494
495 let mut options = self.dynamic_options.read().unwrap().clone();
497 options.insert(SCAN_TIMESTAMP_MILLIS_OPTION.to_string(), millis.to_string());
498
499 let table_with_options = paimon_table.copy_with_options(options);
500 let provider = Arc::new(PaimonTableProvider::try_new(table_with_options)?);
501
502 let uuid_name = format!("__paimon_tt_{}", uuid::Uuid::new_v4().as_simple());
503 self.register_temp_table(uuid_name.as_str(), provider)?;
504 tracker.register(&uuid_name);
505 replacements.push((info.clause_range, uuid_name));
506 }
507
508 replacements.sort_by_key(|r| std::cmp::Reverse(r.0 .0));
511
512 let mut rewritten_sql = sql.to_string();
514 for ((start, end), uuid_name) in &replacements {
515 rewritten_sql = format!(
516 "{}{}{}",
517 &rewritten_sql[..*start],
518 uuid_name,
519 &rewritten_sql[*end..]
520 );
521 }
522
523 self.ctx.sql(&rewritten_sql).await
525 }
526
527 fn parse_timestamp_to_millis(ts: &str) -> DFResult<i64> {
529 use chrono::{Local, NaiveDateTime, TimeZone};
530
531 let naive = NaiveDateTime::parse_from_str(ts, "%Y-%m-%d %H:%M:%S").map_err(|e| {
532 DataFusionError::Plan(format!(
533 "Cannot parse time travel timestamp '{ts}': {e}. Expected format: YYYY-MM-DD HH:MM:SS"
534 ))
535 })?;
536 let local = Local.from_local_datetime(&naive).single().ok_or_else(|| {
537 DataFusionError::Plan(format!("Ambiguous or invalid local time: '{ts}'"))
538 })?;
539 Ok(local.timestamp_millis())
540 }
541
542 fn resolve_table_name_from_ref(
544 &self,
545 table_ref: &datafusion::common::TableReference,
546 ) -> DFResult<(Arc<dyn Catalog>, String, Identifier)> {
547 match table_ref {
548 datafusion::common::TableReference::Full {
549 catalog,
550 schema,
551 table,
552 } => {
553 let catalog_arc = self
554 .catalogs
555 .get(catalog.as_ref())
556 .ok_or_else(|| DataFusionError::Plan(format!("Unknown catalog '{catalog}'")))?;
557 Ok((
558 catalog_arc.clone(),
559 catalog.to_string(),
560 Identifier::new(schema.as_ref(), table.as_ref()),
561 ))
562 }
563 datafusion::common::TableReference::Partial { schema, table } => {
564 let catalog = self.current_catalog()?;
565 let catalog_name = self.current_catalog_name();
566 Ok((
567 catalog,
568 catalog_name,
569 Identifier::new(schema.as_ref(), table.as_ref()),
570 ))
571 }
572 datafusion::common::TableReference::Bare { table } => {
573 let catalog = self.current_catalog()?;
574 let catalog_name = self.current_catalog_name();
575 let default_schema = self
576 .ctx
577 .state()
578 .config_options()
579 .catalog
580 .default_schema
581 .clone();
582 Ok((
583 catalog,
584 catalog_name,
585 Identifier::new(default_schema, table.as_ref()),
586 ))
587 }
588 }
589 }
590
591 async fn handle_create_table(
592 &self,
593 catalog: &Arc<dyn Catalog>,
594 ct: &CreateTable,
595 partition_keys: Vec<String>,
596 ) -> DFResult<DataFrame> {
597 if ct.external {
598 return Err(DataFusionError::Plan(
599 "CREATE EXTERNAL TABLE is not supported. Use CREATE TABLE instead.".to_string(),
600 ));
601 }
602 if ct.location.is_some() {
603 return Err(DataFusionError::Plan(
604 "LOCATION is not supported for Paimon tables. Table path is determined by the catalog warehouse.".to_string(),
605 ));
606 }
607 if ct.query.is_some() {
608 return Err(DataFusionError::Plan(
609 "CREATE TABLE AS SELECT is not yet supported for Paimon tables.".to_string(),
610 ));
611 }
612
613 let identifier = self.resolve_table_name(&ct.name)?;
614
615 let mut builder = paimon::spec::Schema::builder();
616
617 for col in &ct.columns {
619 let paimon_type = column_def_to_paimon_type(col)?;
620 builder = builder.column(col.name.value.clone(), paimon_type);
621 }
622
623 for constraint in &ct.constraints {
625 if let datafusion::sql::sqlparser::ast::TableConstraint::PrimaryKey(pk) = constraint {
626 let pk_cols: Vec<String> = pk
627 .columns
628 .iter()
629 .map(|c| c.column.expr.to_string())
630 .collect();
631 builder = builder.primary_key(pk_cols);
632 }
633 }
634
635 if !partition_keys.is_empty() {
637 let col_names: Vec<&str> = ct.columns.iter().map(|c| c.name.value.as_str()).collect();
638 for pk in &partition_keys {
639 if !col_names.contains(&pk.as_str()) {
640 return Err(DataFusionError::Plan(format!(
641 "PARTITIONED BY column '{pk}' is not defined in the table"
642 )));
643 }
644 }
645 builder = builder.partition_keys(partition_keys);
646 }
647
648 for (k, v) in extract_options(&ct.table_options)? {
650 builder = builder.option(k, v);
651 }
652
653 let schema = builder.build().map_err(to_datafusion_error)?;
654
655 catalog
656 .create_table(&identifier, schema, ct.if_not_exists)
657 .await
658 .map_err(to_datafusion_error)?;
659
660 ok_result(&self.ctx)
661 }
662
663 async fn handle_create_temp_table(&self, ct: &CreateTable) -> DFResult<DataFrame> {
664 let table_ref: TableReference = ct.name.to_string().as_str().into();
665
666 if ct.if_not_exists && self.temp_table_exist(table_ref.clone())? {
667 return ok_result(&self.ctx);
668 }
669
670 let declared_schema = if !ct.columns.is_empty() {
672 let fields: Vec<Field> = ct
673 .columns
674 .iter()
675 .map(|col| {
676 let paimon_type =
677 sql_data_type_to_paimon_type(&col.data_type, column_def_nullable(col))?;
678 let arrow_type = paimon::arrow::paimon_type_to_arrow(&paimon_type)
679 .map_err(to_datafusion_error)?;
680 Ok(Field::new(
681 &col.name.value,
682 arrow_type,
683 column_def_nullable(col),
684 ))
685 })
686 .collect::<DFResult<Vec<_>>>()?;
687 Some(Arc::new(Schema::new(fields)))
688 } else {
689 None
690 };
691
692 if let Some(query) = &ct.query {
693 let query_sql = query.to_string();
695 let df = self.ctx.sql(&query_sql).await?;
696 let schema = df.schema().inner().clone();
697 let batches = df.collect().await?;
698
699 let batches = if ct.columns.is_empty() {
701 batches
702 } else {
703 let target_fields: Vec<(String, ArrowDataType)> = ct
704 .columns
705 .iter()
706 .map(|col| {
707 let paimon_type =
708 sql_data_type_to_paimon_type(&col.data_type, column_def_nullable(col))?;
709 let arrow_type = paimon::arrow::paimon_type_to_arrow(&paimon_type)
710 .map_err(to_datafusion_error)?;
711 Ok((col.name.value.clone(), arrow_type))
712 })
713 .collect::<DFResult<Vec<_>>>()?;
714
715 let select_col_count = schema.fields().len();
716 let declared_col_count = target_fields.len();
717 if select_col_count < declared_col_count {
718 return Err(DataFusionError::Plan(format!(
719 "CREATE TEMPORARY TABLE AS SELECT: declared {declared_col_count} column(s) \
720 but SELECT query returns only {select_col_count} column(s)"
721 )));
722 }
723
724 batches
725 .into_iter()
726 .map(|batch| {
727 let columns = batch
728 .columns()
729 .iter()
730 .enumerate()
731 .map(|(i, col)| {
732 if i < target_fields.len() {
733 let target_dt = &target_fields[i].1;
734 if *col.data_type() != *target_dt {
735 cast(col, target_dt)
736 .map_err(|e| DataFusionError::External(e.into()))
737 } else {
738 Ok(col.clone())
739 }
740 } else {
741 Ok(col.clone())
742 }
743 })
744 .collect::<DFResult<Vec<_>>>()?;
745 let new_fields = target_fields
746 .iter()
747 .zip(schema.fields().iter())
748 .map(|((name, dt), _)| Field::new(name, dt.clone(), true))
749 .chain(
750 schema
751 .fields()
752 .iter()
753 .skip(target_fields.len())
754 .map(|f| f.as_ref().clone()),
755 )
756 .collect::<Vec<_>>();
757 let new_schema = Schema::new(new_fields);
758 RecordBatch::try_new(Arc::new(new_schema), columns)
759 .map_err(|e| DataFusionError::External(e.into()))
760 })
761 .collect::<DFResult<Vec<_>>>()?
762 };
763
764 let schema = batches.first().map(|b| b.schema()).unwrap_or(schema);
765 let mem_table = MemTable::try_new(schema, vec![batches])?;
766 self.register_temp_table(table_ref, Arc::new(mem_table))?;
767 } else if let Some(schema) = declared_schema {
768 let mem_table = MemTable::try_new(schema, vec![vec![]])?;
770 self.register_temp_table(table_ref, Arc::new(mem_table))?;
771 } else {
772 return Err(DataFusionError::Plan(
773 "CREATE TEMPORARY TABLE requires column definitions or AS SELECT".to_string(),
774 ));
775 }
776
777 ok_result(&self.ctx)
778 }
779
780 fn handle_drop_temp_table(&self, names: &[ObjectName], if_exists: bool) -> DFResult<DataFrame> {
781 for name in names {
782 let table_ref: TableReference = name.to_string().as_str().into();
783 if if_exists && !self.temp_table_exist(table_ref.clone())? {
784 continue;
785 }
786 self.deregister_temp_table(table_ref)?;
787 }
788 ok_result(&self.ctx)
789 }
790
791 async fn handle_drop_table(
792 &self,
793 catalog: &Arc<dyn Catalog>,
794 names: &[ObjectName],
795 if_exists: bool,
796 ) -> DFResult<DataFrame> {
797 for name in names {
798 let identifier = self.resolve_table_name(name)?;
799 catalog
800 .drop_table(&identifier, if_exists)
801 .await
802 .map_err(|e| DataFusionError::External(Box::new(e)))?;
803 }
804 ok_result(&self.ctx)
805 }
806
807 async fn handle_alter_table(
808 &self,
809 catalog: &Arc<dyn Catalog>,
810 name: &ObjectName,
811 operations: &[AlterTableOperation],
812 if_exists: bool,
813 ) -> DFResult<DataFrame> {
814 let identifier = self.resolve_table_name(name)?;
815
816 let mut changes = Vec::new();
817 let mut rename_to: Option<Identifier> = None;
818
819 for op in operations {
820 match op {
821 AlterTableOperation::AddColumn { column_def, .. } => {
822 let change = column_def_to_add_column(column_def)?;
823 changes.push(change);
824 }
825 AlterTableOperation::DropColumn {
826 column_names,
827 if_exists: _,
828 ..
829 } => {
830 for col in column_names {
831 changes.push(SchemaChange::drop_column(col.value.clone()));
832 }
833 }
834 AlterTableOperation::RenameColumn {
835 old_column_name,
836 new_column_name,
837 } => {
838 changes.push(SchemaChange::rename_column(
839 old_column_name.value.clone(),
840 new_column_name.value.clone(),
841 ));
842 }
843 AlterTableOperation::RenameTable { table_name } => {
844 let new_name = match table_name {
845 RenameTableNameKind::To(name) | RenameTableNameKind::As(name) => {
846 object_name_to_string(name)
847 }
848 };
849 rename_to = Some(Identifier::new(identifier.database().to_string(), new_name));
850 }
851 AlterTableOperation::SetTblProperties { table_properties } => {
852 for opt in table_properties {
853 if let SqlOption::KeyValue { key, value } = opt {
854 let v = value.to_string();
855 let v = v
856 .strip_prefix('\'')
857 .and_then(|s| s.strip_suffix('\''))
858 .unwrap_or(&v)
859 .to_string();
860 changes.push(SchemaChange::set_option(key.value.clone(), v));
861 }
862 }
863 }
864 AlterTableOperation::DropPartitions {
865 partitions,
866 if_exists: partition_if_exists,
867 } => {
868 return self
869 .handle_drop_partitions(
870 catalog,
871 &identifier,
872 partitions,
873 if_exists || *partition_if_exists,
874 )
875 .await;
876 }
877 other => {
878 return Err(DataFusionError::Plan(format!(
879 "Unsupported ALTER TABLE operation: {other}"
880 )));
881 }
882 }
883 }
884
885 if let Some(new_identifier) = rename_to {
886 catalog
887 .rename_table(&identifier, &new_identifier, if_exists)
888 .await
889 .map_err(to_datafusion_error)?;
890 }
891
892 if !changes.is_empty() {
893 catalog
894 .alter_table(&identifier, changes, if_exists)
895 .await
896 .map_err(to_datafusion_error)?;
897 }
898
899 ok_result(&self.ctx)
900 }
901
902 async fn handle_merge_into(&self, merge: &Merge) -> DFResult<DataFrame> {
903 let table_name = match &merge.table {
904 TableFactor::Table { name, .. } => name.clone(),
905 other => {
906 return Err(DataFusionError::Plan(format!(
907 "Unsupported target table in MERGE INTO: {other}"
908 )))
909 }
910 };
911 let (catalog, _catalog_name, identifier) = self.resolve_catalog_and_table(&table_name)?;
912
913 let table = catalog
914 .get_table(&identifier)
915 .await
916 .map_err(to_datafusion_error)?;
917
918 crate::merge_into::execute_merge_into(self, merge, table).await
919 }
920
921 async fn handle_update(&self, update: &Update) -> DFResult<DataFrame> {
922 let table_name = match &update.table.relation {
923 TableFactor::Table { name, .. } => name.clone(),
924 other => {
925 return Err(DataFusionError::Plan(format!(
926 "Unsupported target table in UPDATE: {other}"
927 )))
928 }
929 };
930 let (catalog, _catalog_name, identifier) = self.resolve_catalog_and_table(&table_name)?;
931
932 let table = catalog
933 .get_table(&identifier)
934 .await
935 .map_err(to_datafusion_error)?;
936
937 crate::update::execute_update(self, update, table).await
938 }
939
940 async fn handle_delete(&self, delete: &Delete) -> DFResult<DataFrame> {
941 let tables = match &delete.from {
942 FromTable::WithFromKeyword(t) | FromTable::WithoutKeyword(t) => t,
943 };
944 let table_factor = tables
945 .first()
946 .map(|t| &t.relation)
947 .ok_or_else(|| DataFusionError::Plan("DELETE requires a target table".to_string()))?;
948 let table_name = match table_factor {
949 TableFactor::Table { name, .. } => name.clone(),
950 other => {
951 return Err(DataFusionError::Plan(format!(
952 "Unsupported target table in DELETE: {other}"
953 )))
954 }
955 };
956 let (catalog, _catalog_name, identifier) = self.resolve_catalog_and_table(&table_name)?;
957
958 let table = catalog
959 .get_table(&identifier)
960 .await
961 .map_err(to_datafusion_error)?;
962
963 let table_ref = table_name.to_string();
964 crate::delete::execute_delete(self, delete, table, &table_ref).await
965 }
966
967 async fn handle_insert_overwrite_partition(&self, insert: &Insert) -> DFResult<DataFrame> {
968 let table_name = match &insert.table {
969 TableObject::TableName(name) => name.clone(),
970 other => {
971 return Err(DataFusionError::Plan(format!(
972 "Unsupported target table in INSERT OVERWRITE: {other}"
973 )))
974 }
975 };
976 let (catalog, _catalog_name, identifier) = self.resolve_catalog_and_table(&table_name)?;
977 let table = catalog
978 .get_table(&identifier)
979 .await
980 .map_err(to_datafusion_error)?;
981
982 let partition_exprs = insert.partitioned.as_ref().ok_or_else(|| {
983 DataFusionError::Plan("INSERT OVERWRITE PARTITION requires a PARTITION clause".into())
984 })?;
985 let partition_fields = table.schema().partition_fields();
986 let static_partitions =
987 parse_static_partitions(partition_exprs, &partition_fields, table.schema().fields())?;
988
989 let source = insert.source.as_ref().ok_or_else(|| {
990 DataFusionError::Plan("INSERT OVERWRITE requires a source query".into())
991 })?;
992 let df = self.ctx.sql(&source.to_string()).await?;
993
994 let all_fields = table.schema().fields();
995 let non_static_fields: Vec<&PaimonDataField> = all_fields
996 .iter()
997 .filter(|f| !static_partitions.contains_key(f.name()))
998 .collect();
999 let expected_source_cols = non_static_fields.len();
1000
1001 let target_columns = if !insert.columns.is_empty() {
1004 Some(&insert.columns)
1005 } else if !insert.after_columns.is_empty() {
1006 Some(&insert.after_columns)
1007 } else {
1008 None
1009 };
1010 let column_reorder: Option<Vec<usize>> = if let Some(cols) = target_columns {
1011 if cols.len() != expected_source_cols {
1012 return Err(DataFusionError::Plan(format!(
1013 "Column list has {} columns, but expected {} non-partition columns",
1014 cols.len(),
1015 expected_source_cols
1016 )));
1017 }
1018 let col_names: Vec<&str> = cols.iter().map(|id| id.value.as_str()).collect();
1019 let mut reorder = Vec::with_capacity(expected_source_cols);
1020 for field in &non_static_fields {
1021 let pos = col_names
1022 .iter()
1023 .position(|c| c == &field.name())
1024 .ok_or_else(|| {
1025 DataFusionError::Plan(format!(
1026 "Column '{}' not found in target column list",
1027 field.name()
1028 ))
1029 })?;
1030 reorder.push(pos);
1031 }
1032 Some(reorder)
1033 } else {
1034 None
1035 };
1036
1037 let source_col_count = df.schema().fields().len();
1039 if source_col_count != expected_source_cols {
1040 return Err(DataFusionError::Plan(format!(
1041 "Source query has {} columns, but expected {} non-partition columns",
1042 source_col_count, expected_source_cols
1043 )));
1044 }
1045
1046 let mut stream = df.execute_stream().await?;
1047
1048 let wb = table.new_write_builder();
1049 let mut tw = wb
1050 .new_write()
1051 .map_err(to_datafusion_error)?
1052 .with_overwrite();
1053 let mut row_count = 0u64;
1054
1055 while let Some(batch_result) = stream.next().await {
1056 let batch = batch_result?;
1057 if batch.num_rows() == 0 {
1058 continue;
1059 }
1060 let batch = if let Some(ref reorder) = column_reorder {
1061 let reordered_cols: Vec<ArrayRef> =
1062 reorder.iter().map(|&i| batch.column(i).clone()).collect();
1063 let reordered_fields: Vec<Field> = reorder
1064 .iter()
1065 .map(|&i| batch.schema().field(i).clone())
1066 .collect();
1067 let reordered_schema = Arc::new(Schema::new(reordered_fields));
1068 RecordBatch::try_new(reordered_schema, reordered_cols)
1069 .map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?
1070 } else {
1071 batch
1072 };
1073 let augmented = append_partition_columns(
1074 &batch,
1075 &static_partitions,
1076 expected_source_cols,
1077 all_fields,
1078 )?;
1079 row_count += augmented.num_rows() as u64;
1080 tw.write_arrow_batch(&augmented)
1081 .await
1082 .map_err(to_datafusion_error)?;
1083 }
1084
1085 let messages = tw.prepare_commit().await.map_err(to_datafusion_error)?;
1086 let commit = wb.new_commit();
1087
1088 let overwrite_partitions = if static_partitions.is_empty() {
1089 None
1090 } else {
1091 Some(static_partitions)
1092 };
1093 commit
1094 .overwrite(messages, overwrite_partitions)
1095 .await
1096 .map_err(to_datafusion_error)?;
1097
1098 crate::merge_into::ok_result(&self.ctx, row_count)
1099 }
1100
1101 async fn handle_truncate_table(&self, truncate: &Truncate) -> DFResult<DataFrame> {
1102 if truncate.table_names.len() > 1 {
1103 return Err(DataFusionError::Plan(
1104 "TRUNCATE TABLE does not support multiple tables".to_string(),
1105 ));
1106 }
1107 let target = truncate.table_names.first().ok_or_else(|| {
1108 DataFusionError::Plan("TRUNCATE TABLE requires a table name".to_string())
1109 })?;
1110 let (catalog, _catalog_name, identifier) = self.resolve_catalog_and_table(&target.name)?;
1111 let table = match catalog.get_table(&identifier).await {
1112 Ok(t) => t,
1113 Err(e) if truncate.if_exists && is_table_not_exist(&e) => {
1114 return ok_result(&self.ctx);
1115 }
1116 Err(e) => return Err(to_datafusion_error(e)),
1117 };
1118
1119 let wb = table.new_write_builder();
1120 let commit = wb.new_commit();
1121
1122 if let Some(partitions) = &truncate.partitions {
1123 if partitions.is_empty() {
1124 return Err(DataFusionError::Plan(
1125 "PARTITION clause requires at least one column = value".to_string(),
1126 ));
1127 }
1128 let partition_values = parse_partition_values(
1129 partitions,
1130 table.schema().fields(),
1131 table.schema().partition_keys(),
1132 )?;
1133 commit
1134 .truncate_partitions(partition_values)
1135 .await
1136 .map_err(to_datafusion_error)?;
1137 return ok_result(&self.ctx);
1138 }
1139
1140 commit.truncate_table().await.map_err(to_datafusion_error)?;
1141 ok_result(&self.ctx)
1142 }
1143
1144 async fn handle_create_view(&self, create_view: &CreateView) -> DFResult<DataFrame> {
1145 if create_view.materialized {
1146 return Err(DataFusionError::Plan(
1147 "CREATE MATERIALIZED VIEW is not supported".to_string(),
1148 ));
1149 }
1150
1151 let view_name = create_view.name.to_string();
1152 let table_ref: TableReference = view_name.as_str().into();
1153 let (catalog, database, name) = self.resolve_temp_table_name(table_ref)?;
1154
1155 let query_sql = create_view.query.to_string();
1157 let df = self.ctx.sql(&query_sql).await?;
1158 let logical_plan = df.logical_plan().clone();
1159
1160 if create_view.temporary {
1161 if create_view.if_not_exists
1162 && self.temp_table_exist(format!("{catalog}.{database}.{name}"))?
1163 {
1164 return ok_result(&self.ctx);
1165 }
1166 let view_table = datafusion::datasource::ViewTable::new(logical_plan, Some(query_sql));
1168 self.register_temp_table(format!("{catalog}.{database}.{name}"), Arc::new(view_table))?;
1169 ok_result(&self.ctx)
1170 } else {
1171 Err(DataFusionError::Plan(
1172 "CREATE VIEW (non-temporary) is not supported. Use CREATE TEMPORARY VIEW instead."
1173 .to_string(),
1174 ))
1175 }
1176 }
1177
1178 async fn handle_drop_partitions(
1179 &self,
1180 catalog: &Arc<dyn Catalog>,
1181 identifier: &Identifier,
1182 partitions: &[SqlExpr],
1183 if_exists: bool,
1184 ) -> DFResult<DataFrame> {
1185 if partitions.is_empty() {
1186 return Err(DataFusionError::Plan(
1187 "DROP PARTITIONS requires at least one partition specification".to_string(),
1188 ));
1189 }
1190 let table = match catalog.get_table(identifier).await {
1191 Ok(t) => t,
1192 Err(e) if if_exists && is_table_not_exist(&e) => {
1193 return ok_result(&self.ctx);
1194 }
1195 Err(e) => return Err(to_datafusion_error(e)),
1196 };
1197
1198 let partition_values = parse_partition_values(
1199 partitions,
1200 table.schema().fields(),
1201 table.schema().partition_keys(),
1202 )?;
1203
1204 let wb = table.new_write_builder();
1205 let commit = wb.new_commit();
1206 commit
1207 .truncate_partitions(partition_values)
1208 .await
1209 .map_err(to_datafusion_error)?;
1210
1211 ok_result(&self.ctx)
1212 }
1213
1214 pub(crate) fn current_catalog_name(&self) -> String {
1216 self.ctx
1217 .state()
1218 .config_options()
1219 .catalog
1220 .default_catalog
1221 .clone()
1222 }
1223
1224 fn current_catalog(&self) -> DFResult<Arc<dyn Catalog>> {
1225 let name = self.current_catalog_name();
1226 self.catalogs.get(&name).cloned().ok_or_else(|| {
1227 DataFusionError::Plan(
1228 "No catalog registered. Call register_catalog() first.".to_string(),
1229 )
1230 })
1231 }
1232
1233 fn is_paimon_catalog_ref(&self, table_ref: &TableReference) -> bool {
1235 let catalog_name = match table_ref {
1236 TableReference::Full { catalog, .. } => catalog.to_string(),
1237 TableReference::Partial { .. } | TableReference::Bare { .. } => {
1238 self.current_catalog_name()
1239 }
1240 };
1241 self.catalogs.contains_key(&catalog_name)
1242 }
1243
1244 fn resolve_catalog_and_table(
1246 &self,
1247 name: &ObjectName,
1248 ) -> DFResult<(Arc<dyn Catalog>, String, Identifier)> {
1249 let parts: Vec<String> = name
1250 .0
1251 .iter()
1252 .filter_map(|p| p.as_ident().map(|id| id.value.clone()))
1253 .collect();
1254 match parts.len() {
1255 3 => {
1256 let catalog = self.catalogs.get(&parts[0]).ok_or_else(|| {
1257 DataFusionError::Plan(format!("Unknown catalog '{}'", parts[0]))
1258 })?;
1259 Ok((
1260 catalog.clone(),
1261 parts[0].clone(),
1262 Identifier::new(parts[1].clone(), parts[2].clone()),
1263 ))
1264 }
1265 2 => {
1266 let catalog = self.current_catalog()?;
1267 Ok((
1268 catalog,
1269 self.current_catalog_name(),
1270 Identifier::new(parts[0].clone(), parts[1].clone()),
1271 ))
1272 }
1273 1 => {
1274 let catalog = self.current_catalog()?;
1275 let default_schema = self
1276 .ctx
1277 .state()
1278 .config_options()
1279 .catalog
1280 .default_schema
1281 .clone();
1282 Ok((
1283 catalog,
1284 self.current_catalog_name(),
1285 Identifier::new(default_schema, parts[0].clone()),
1286 ))
1287 }
1288 _ => Err(DataFusionError::Plan(format!(
1289 "Invalid table reference: {name}"
1290 ))),
1291 }
1292 }
1293
1294 fn resolve_table_name(&self, name: &ObjectName) -> DFResult<Identifier> {
1296 let (_catalog, _catalog_name, identifier) = self.resolve_catalog_and_table(name)?;
1297 Ok(identifier)
1298 }
1299}
1300
1301fn looks_like_create_table(sql: &str) -> bool {
1304 let bytes = sql.as_bytes();
1305 let len = bytes.len();
1306 let mut i = 0;
1307 loop {
1309 while i < len && bytes[i].is_ascii_whitespace() {
1310 i += 1;
1311 }
1312 if i + 1 < len && bytes[i] == b'-' && bytes[i + 1] == b'-' {
1313 i += 2;
1314 while i < len && bytes[i] != b'\n' {
1315 i += 1;
1316 }
1317 continue;
1318 }
1319 if i + 1 < len && bytes[i] == b'/' && bytes[i + 1] == b'*' {
1320 i += 2;
1321 while i + 1 < len {
1322 if bytes[i] == b'*' && bytes[i + 1] == b'/' {
1323 i += 2;
1324 break;
1325 }
1326 i += 1;
1327 }
1328 continue;
1329 }
1330 break;
1331 }
1332 if i + 6 > len || !bytes[i..i + 6].eq_ignore_ascii_case(b"CREATE") {
1334 return false;
1335 }
1336 i += 6;
1337 if i >= len || !bytes[i].is_ascii_whitespace() {
1338 return false;
1339 }
1340 while i < len && bytes[i].is_ascii_whitespace() {
1341 i += 1;
1342 }
1343 if i + 9 <= len && bytes[i..i + 9].eq_ignore_ascii_case(b"TEMPORARY") {
1345 i += 9;
1346 while i < len && bytes[i].is_ascii_whitespace() {
1347 i += 1;
1348 }
1349 } else if i + 4 <= len && bytes[i..i + 4].eq_ignore_ascii_case(b"TEMP") {
1350 i += 4;
1351 while i < len && bytes[i].is_ascii_whitespace() {
1352 i += 1;
1353 }
1354 }
1355 if i + 4 <= len && bytes[i..i + 4].eq_ignore_ascii_case(b"VIEW") {
1357 return false;
1358 }
1359 i + 5 <= len && bytes[i..i + 5].eq_ignore_ascii_case(b"TABLE")
1360}
1361
1362fn find_partitioned_by(sql: &str) -> Option<(usize, usize)> {
1364 let bytes = sql.as_bytes();
1365 let len = bytes.len();
1366 let mut i = 0;
1367 while i < len {
1368 match bytes[i] {
1369 b'\'' => {
1370 i += 1;
1371 while i < len {
1372 if bytes[i] == b'\'' {
1373 i += 1;
1374 if i < len && bytes[i] == b'\'' {
1375 i += 1;
1376 } else {
1377 break;
1378 }
1379 } else {
1380 i += 1;
1381 }
1382 }
1383 }
1384 b'-' if i + 1 < len && bytes[i + 1] == b'-' => {
1385 i += 2;
1386 while i < len && bytes[i] != b'\n' {
1387 i += 1;
1388 }
1389 }
1390 b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
1391 i += 2;
1392 while i + 1 < len {
1393 if bytes[i] == b'*' && bytes[i + 1] == b'/' {
1394 i += 2;
1395 break;
1396 }
1397 i += 1;
1398 }
1399 }
1400 b if b.is_ascii_alphabetic() && i + 11 <= len => {
1401 if bytes[i..i + 11].eq_ignore_ascii_case(b"PARTITIONED") {
1402 let rest = &bytes[i + 11..];
1403 let ws = rest.iter().take_while(|b| b.is_ascii_whitespace()).count();
1404 if ws > 0
1405 && i + 11 + ws + 2 <= len
1406 && rest[ws..ws + 2].eq_ignore_ascii_case(b"BY")
1407 {
1408 let by_end = i + 11 + ws + 2;
1409 return Some((i, by_end));
1410 }
1411 }
1412 i += 1;
1413 }
1414 _ => {
1415 i += 1;
1416 }
1417 }
1418 }
1419 None
1420}
1421
1422fn parse_partition_column(token: &str) -> DFResult<String> {
1424 let trimmed = token.trim();
1425 if trimmed.is_empty() {
1426 return Err(DataFusionError::Plan(
1427 "Empty column name in PARTITIONED BY".to_string(),
1428 ));
1429 }
1430
1431 let first = trimmed.as_bytes()[0];
1432 if first == b'"' || first == b'`' {
1433 let close = if first == b'"' { b'"' } else { b'`' };
1434 if let Some(end) = trimmed[1..].find(close as char) {
1435 let after_quote = trimmed[1 + end + 1..].trim();
1436 if after_quote.is_empty() {
1437 return Ok(trimmed[1..1 + end].to_string());
1438 }
1439 }
1440 return Err(DataFusionError::Plan(format!(
1441 "Invalid quoted identifier in PARTITIONED BY: {trimmed}"
1442 )));
1443 }
1444
1445 let parts: Vec<&str> = trimmed.split_whitespace().collect();
1446 match parts.len() {
1447 1 => Ok(parts[0].to_string()),
1448 _ => Err(DataFusionError::Plan(format!(
1449 "PARTITIONED BY column '{}' should not specify a type. \
1450 Use column references only, e.g. PARTITIONED BY ({})",
1451 parts[0], parts[0]
1452 ))),
1453 }
1454}
1455
1456fn extract_partition_by(sql: &str) -> DFResult<(String, Vec<String>)> {
1463 let Some((kw_start, by_end)) = find_partitioned_by(sql) else {
1464 return Ok((sql.to_string(), vec![]));
1465 };
1466
1467 let after_by = sql[by_end..].trim_start();
1468 let paren_start = by_end + (sql[by_end..].len() - after_by.len());
1469
1470 if !after_by.starts_with('(') {
1471 return Err(DataFusionError::Plan(
1472 "Expected '(' after PARTITIONED BY".to_string(),
1473 ));
1474 }
1475
1476 let inner_start = paren_start + 1;
1477 let mut depth = 1;
1478 let mut paren_end = None;
1479 for (i, ch) in sql[inner_start..].char_indices() {
1480 match ch {
1481 '(' => depth += 1,
1482 ')' => {
1483 depth -= 1;
1484 if depth == 0 {
1485 paren_end = Some(inner_start + i);
1486 break;
1487 }
1488 }
1489 _ => {}
1490 }
1491 }
1492 let paren_end = paren_end.ok_or_else(|| {
1493 DataFusionError::Plan("Unmatched '(' in PARTITIONED BY clause".to_string())
1494 })?;
1495
1496 let inner = sql[inner_start..paren_end].trim();
1497 if inner.is_empty() {
1498 return Err(DataFusionError::Plan(
1499 "PARTITIONED BY must specify at least one column".to_string(),
1500 ));
1501 }
1502
1503 let mut partition_keys = Vec::new();
1504 for token in inner.split(',') {
1505 partition_keys.push(parse_partition_column(token)?);
1506 }
1507
1508 let clause_end = paren_end + 1;
1509 let mut rewritten = String::with_capacity(sql.len());
1510 rewritten.push_str(&sql[..kw_start]);
1511 rewritten.push_str(&sql[clause_end..]);
1512 Ok((rewritten, partition_keys))
1513}
1514
1515fn column_def_to_add_column(col: &ColumnDef) -> DFResult<SchemaChange> {
1517 let paimon_type = column_def_to_paimon_type(col)?;
1518 Ok(SchemaChange::add_column(
1519 col.name.value.clone(),
1520 paimon_type,
1521 ))
1522}
1523
1524fn column_def_to_paimon_type(col: &ColumnDef) -> DFResult<PaimonDataType> {
1525 sql_data_type_to_paimon_type(&col.data_type, column_def_nullable(col))
1526}
1527
1528fn column_def_nullable(col: &ColumnDef) -> bool {
1529 !col.options.iter().any(|opt| {
1530 matches!(
1531 opt.option,
1532 datafusion::sql::sqlparser::ast::ColumnOption::NotNull
1533 )
1534 })
1535}
1536
1537fn sql_data_type_to_paimon_type(
1542 sql_type: &datafusion::sql::sqlparser::ast::DataType,
1543 nullable: bool,
1544) -> DFResult<PaimonDataType> {
1545 use datafusion::sql::sqlparser::ast::{
1546 ArrayElemTypeDef, DataType as SqlType, ExactNumberInfo, TimezoneInfo,
1547 };
1548
1549 match sql_type {
1550 SqlType::Boolean => Ok(PaimonDataType::Boolean(BooleanType::with_nullable(
1551 nullable,
1552 ))),
1553 SqlType::TinyInt(_) => Ok(PaimonDataType::TinyInt(TinyIntType::with_nullable(
1554 nullable,
1555 ))),
1556 SqlType::SmallInt(_) => Ok(PaimonDataType::SmallInt(SmallIntType::with_nullable(
1557 nullable,
1558 ))),
1559 SqlType::Int(_) | SqlType::Integer(_) => {
1560 Ok(PaimonDataType::Int(IntType::with_nullable(nullable)))
1561 }
1562 SqlType::BigInt(_) => Ok(PaimonDataType::BigInt(BigIntType::with_nullable(nullable))),
1563 SqlType::Float(_) | SqlType::Real => {
1564 Ok(PaimonDataType::Float(FloatType::with_nullable(nullable)))
1565 }
1566 SqlType::Double(_) | SqlType::DoublePrecision => {
1567 Ok(PaimonDataType::Double(DoubleType::with_nullable(nullable)))
1568 }
1569 SqlType::Varchar(_)
1570 | SqlType::CharVarying(_)
1571 | SqlType::Text
1572 | SqlType::String(_)
1573 | SqlType::Char(_)
1574 | SqlType::Character(_) => Ok(PaimonDataType::VarChar(
1575 VarCharType::with_nullable(nullable, VarCharType::MAX_LENGTH)
1576 .map_err(to_datafusion_error)?,
1577 )),
1578 SqlType::Binary(_) | SqlType::Varbinary(_) | SqlType::Bytea => {
1579 Ok(PaimonDataType::VarBinary(
1580 VarBinaryType::try_new(nullable, VarBinaryType::MAX_LENGTH)
1581 .map_err(to_datafusion_error)?,
1582 ))
1583 }
1584 SqlType::Blob(_) => Ok(PaimonDataType::Blob(BlobType::with_nullable(nullable))),
1585 SqlType::Date => Ok(PaimonDataType::Date(DateType::with_nullable(nullable))),
1586 SqlType::Timestamp(precision, tz_info) => {
1587 let precision = match precision {
1588 Some(0) => 0,
1589 Some(1..=3) | None => 3,
1590 Some(4..=6) => 6,
1591 _ => 9,
1592 };
1593 match tz_info {
1594 TimezoneInfo::None | TimezoneInfo::WithoutTimeZone => {
1595 Ok(PaimonDataType::Timestamp(
1596 TimestampType::with_nullable(nullable, precision)
1597 .map_err(to_datafusion_error)?,
1598 ))
1599 }
1600 _ => Ok(PaimonDataType::LocalZonedTimestamp(
1601 LocalZonedTimestampType::with_nullable(nullable, precision)
1602 .map_err(to_datafusion_error)?,
1603 )),
1604 }
1605 }
1606 SqlType::Decimal(info) => {
1607 let (precision, scale) = match info {
1608 ExactNumberInfo::PrecisionAndScale(precision, scale) => {
1609 (*precision as u32, *scale as u32)
1610 }
1611 ExactNumberInfo::Precision(precision) => (*precision as u32, 0),
1612 ExactNumberInfo::None => (10, 0),
1613 };
1614 Ok(PaimonDataType::Decimal(
1615 DecimalType::with_nullable(nullable, precision, scale)
1616 .map_err(to_datafusion_error)?,
1617 ))
1618 }
1619 SqlType::Array(elem_def) => {
1620 let element_type = match elem_def {
1621 ArrayElemTypeDef::AngleBracket(t)
1622 | ArrayElemTypeDef::SquareBracket(t, _)
1623 | ArrayElemTypeDef::Parenthesis(t) => sql_data_type_to_paimon_type(t, true)?,
1624 ArrayElemTypeDef::None => {
1625 return Err(DataFusionError::Plan(
1626 "ARRAY type requires an element type".to_string(),
1627 ));
1628 }
1629 };
1630 Ok(PaimonDataType::Array(PaimonArrayType::with_nullable(
1631 nullable,
1632 element_type,
1633 )))
1634 }
1635 SqlType::Map(key_type, value_type) => {
1636 let key = sql_data_type_to_paimon_type(key_type, false)?;
1637 let value = sql_data_type_to_paimon_type(value_type, true)?;
1638 Ok(PaimonDataType::Map(PaimonMapType::with_nullable(
1639 nullable, key, value,
1640 )))
1641 }
1642 SqlType::Struct(fields, _) => {
1643 let paimon_fields = fields
1644 .iter()
1645 .enumerate()
1646 .map(|(idx, field)| {
1647 let name = field
1648 .field_name
1649 .as_ref()
1650 .map(|n| n.value.clone())
1651 .unwrap_or_default();
1652 let data_type = sql_data_type_to_paimon_type(&field.field_type, true)?;
1653 Ok(PaimonDataField::new(idx as i32, name, data_type))
1654 })
1655 .collect::<DFResult<Vec<_>>>()?;
1656 Ok(PaimonDataType::Row(PaimonRowType::with_nullable(
1657 nullable,
1658 paimon_fields,
1659 )))
1660 }
1661 _ => Err(DataFusionError::Plan(format!(
1662 "Unsupported SQL data type: {sql_type}"
1663 ))),
1664 }
1665}
1666
1667fn object_name_to_string(name: &ObjectName) -> String {
1668 name.0
1669 .iter()
1670 .filter_map(|p| p.as_ident().map(|id| id.value.clone()))
1671 .collect::<Vec<_>>()
1672 .join(".")
1673}
1674
1675fn extract_options(opts: &CreateTableOptions) -> DFResult<Vec<(String, String)>> {
1677 let sql_options = match opts {
1678 CreateTableOptions::With(options)
1679 | CreateTableOptions::Options(options)
1680 | CreateTableOptions::TableProperties(options)
1681 | CreateTableOptions::Plain(options) => options,
1682 CreateTableOptions::None => return Ok(Vec::new()),
1683 };
1684 sql_options
1685 .iter()
1686 .map(|opt| match opt {
1687 SqlOption::KeyValue { key, value } => {
1688 let v = value.to_string();
1689 let v = v
1691 .strip_prefix('\'')
1692 .and_then(|s| s.strip_suffix('\''))
1693 .unwrap_or(&v)
1694 .to_string();
1695 Ok((key.value.clone(), v))
1696 }
1697 other => Err(DataFusionError::Plan(format!(
1698 "Unsupported table option: {other}"
1699 ))),
1700 })
1701 .collect()
1702}
1703
1704fn is_table_not_exist(e: &paimon::Error) -> bool {
1705 matches!(e, paimon::Error::TableNotExist { .. })
1706}
1707
1708fn parse_partition_values(
1714 exprs: &[SqlExpr],
1715 all_fields: &[PaimonDataField],
1716 partition_keys: &[String],
1717) -> DFResult<Vec<HashMap<String, Option<Datum>>>> {
1718 let field_map: HashMap<&str, &PaimonDataField> =
1719 all_fields.iter().map(|f| (f.name(), f)).collect();
1720
1721 let mut partition = HashMap::new();
1722 for expr in exprs {
1723 let (col_name, val_expr) = match expr {
1724 SqlExpr::BinaryOp {
1725 left,
1726 op: datafusion::sql::sqlparser::ast::BinaryOperator::Eq,
1727 right,
1728 } => {
1729 let col = match left.as_ref() {
1730 SqlExpr::Identifier(ident) => ident.value.clone(),
1731 other => {
1732 return Err(DataFusionError::Plan(format!(
1733 "Expected column name in partition spec, got: {other}"
1734 )))
1735 }
1736 };
1737 (col, right.as_ref())
1738 }
1739 other => {
1740 return Err(DataFusionError::Plan(format!(
1741 "Expected 'column = value' in partition spec, got: {other}"
1742 )))
1743 }
1744 };
1745
1746 if !partition_keys.iter().any(|k| k == &col_name) {
1747 return Err(DataFusionError::Plan(format!(
1748 "Column '{col_name}' is not a partition column"
1749 )));
1750 }
1751
1752 let field = field_map.get(col_name.as_str()).ok_or_else(|| {
1753 DataFusionError::Plan(format!("Column '{col_name}' not found in table schema"))
1754 })?;
1755 let datum = sql_expr_to_datum(val_expr, field.data_type())?;
1756 partition.insert(col_name, Some(datum));
1757 }
1758
1759 let missing: Vec<&str> = partition_keys
1760 .iter()
1761 .filter(|k| !partition.contains_key(k.as_str()))
1762 .map(|k| k.as_str())
1763 .collect();
1764 if !missing.is_empty() {
1765 return Err(DataFusionError::Plan(format!(
1766 "Incomplete partition spec: missing keys [{}]. All partition columns must be specified.",
1767 missing.join(", ")
1768 )));
1769 }
1770
1771 Ok(vec![partition])
1772}
1773
1774fn parse_static_partitions(
1778 exprs: &[SqlExpr],
1779 partition_fields: &[PaimonDataField],
1780 all_fields: &[PaimonDataField],
1781) -> DFResult<HashMap<String, Option<Datum>>> {
1782 let mut result = HashMap::new();
1783 let field_map: HashMap<&str, &PaimonDataField> =
1784 all_fields.iter().map(|f| (f.name(), f)).collect();
1785 let partition_names: Vec<&str> = partition_fields.iter().map(|f| f.name()).collect();
1786
1787 for expr in exprs {
1788 let (col_name, val_expr) = match expr {
1789 SqlExpr::BinaryOp {
1790 left,
1791 op: datafusion::sql::sqlparser::ast::BinaryOperator::Eq,
1792 right,
1793 } => {
1794 let col = match left.as_ref() {
1795 SqlExpr::Identifier(ident) => ident.value.clone(),
1796 other => {
1797 return Err(DataFusionError::Plan(format!(
1798 "Expected column name in PARTITION clause, got: {other}"
1799 )))
1800 }
1801 };
1802 (col, right.as_ref())
1803 }
1804 SqlExpr::Identifier(ident) => {
1807 let col_name = &ident.value;
1808 if !partition_names.contains(&col_name.as_str()) {
1809 return Err(DataFusionError::Plan(format!(
1810 "Column '{col_name}' is not a partition column"
1811 )));
1812 }
1813 continue;
1814 }
1815 other => {
1816 return Err(DataFusionError::Plan(format!(
1817 "Unsupported expression in PARTITION clause: {other}"
1818 )))
1819 }
1820 };
1821
1822 if !partition_names.contains(&col_name.as_str()) {
1823 return Err(DataFusionError::Plan(format!(
1824 "Column '{col_name}' is not a partition column"
1825 )));
1826 }
1827
1828 let field = field_map.get(col_name.as_str()).ok_or_else(|| {
1829 DataFusionError::Plan(format!("Column '{col_name}' not found in table schema"))
1830 })?;
1831 let datum = sql_expr_to_datum(val_expr, field.data_type())?;
1832 result.insert(col_name, Some(datum));
1833 }
1834
1835 Ok(result)
1836}
1837
1838fn sql_expr_to_datum(expr: &SqlExpr, data_type: &PaimonDataType) -> DFResult<Datum> {
1840 let (value, negate) = match expr {
1841 SqlExpr::Value(v) => (&v.value, false),
1842 SqlExpr::UnaryOp {
1843 op: datafusion::sql::sqlparser::ast::UnaryOperator::Minus,
1844 expr: inner,
1845 } => {
1846 if let SqlExpr::Value(v) = inner.as_ref() {
1847 (&v.value, true)
1848 } else {
1849 return Err(DataFusionError::Plan(format!(
1850 "Unsupported partition value expression: {expr}"
1851 )));
1852 }
1853 }
1854 other => {
1855 return Err(DataFusionError::Plan(format!(
1856 "Unsupported partition value expression: {other}"
1857 )))
1858 }
1859 };
1860
1861 match (value, data_type) {
1862 (SqlValue::Number(n, _), _) => parse_number_datum(n, data_type, negate),
1863 (SqlValue::SingleQuotedString(s), PaimonDataType::VarChar(_)) if !negate => {
1864 Ok(Datum::String(s.clone()))
1865 }
1866 (SqlValue::SingleQuotedString(s), PaimonDataType::Date(_)) if !negate => {
1867 let date = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
1868 .map_err(|e| DataFusionError::Plan(format!("Invalid DATE '{s}': {e}")))?;
1869 let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
1870 Ok(Datum::Date((date - epoch).num_days() as i32))
1871 }
1872 (SqlValue::Boolean(b), PaimonDataType::Boolean(_)) if !negate => Ok(Datum::Bool(*b)),
1873 _ if negate => Err(DataFusionError::Plan(format!(
1874 "Cannot negate value for type {data_type:?}"
1875 ))),
1876 _ => Err(DataFusionError::Plan(format!(
1877 "Cannot convert {value} to {data_type:?}"
1878 ))),
1879 }
1880}
1881
1882fn parse_number_datum(n: &str, data_type: &PaimonDataType, negate: bool) -> DFResult<Datum> {
1883 let s: String = if negate {
1884 format!("-{n}")
1885 } else {
1886 n.to_string()
1887 };
1888 match data_type {
1889 PaimonDataType::TinyInt(_) => {
1890 Ok(Datum::TinyInt(s.parse::<i8>().map_err(|e| {
1891 DataFusionError::Plan(format!("Invalid TINYINT: {e}"))
1892 })?))
1893 }
1894 PaimonDataType::SmallInt(_) => {
1895 Ok(Datum::SmallInt(s.parse::<i16>().map_err(|e| {
1896 DataFusionError::Plan(format!("Invalid SMALLINT: {e}"))
1897 })?))
1898 }
1899 PaimonDataType::Int(_) => {
1900 Ok(Datum::Int(s.parse::<i32>().map_err(|e| {
1901 DataFusionError::Plan(format!("Invalid INT: {e}"))
1902 })?))
1903 }
1904 PaimonDataType::BigInt(_) => {
1905 Ok(Datum::Long(s.parse::<i64>().map_err(|e| {
1906 DataFusionError::Plan(format!("Invalid BIGINT: {e}"))
1907 })?))
1908 }
1909 PaimonDataType::Float(_) => {
1910 Ok(Datum::Float(s.parse::<f32>().map_err(|e| {
1911 DataFusionError::Plan(format!("Invalid FLOAT: {e}"))
1912 })?))
1913 }
1914 PaimonDataType::Double(_) => {
1915 Ok(Datum::Double(s.parse::<f64>().map_err(|e| {
1916 DataFusionError::Plan(format!("Invalid DOUBLE: {e}"))
1917 })?))
1918 }
1919 _ => Err(DataFusionError::Plan(format!(
1920 "Cannot convert {n} to {data_type:?}"
1921 ))),
1922 }
1923}
1924
1925fn append_partition_columns(
1927 batch: &RecordBatch,
1928 partitions: &HashMap<String, Option<Datum>>,
1929 expected_source_cols: usize,
1930 all_fields: &[PaimonDataField],
1931) -> DFResult<RecordBatch> {
1932 let num_rows = batch.num_rows();
1933
1934 let mut columns: Vec<(String, ArrayRef)> = Vec::with_capacity(all_fields.len());
1935
1936 let mut source_col_idx = 0;
1937 for field in all_fields {
1938 let name = field.name().to_string();
1939 if let Some(datum_opt) = partitions.get(&name) {
1940 let array = datum_to_constant_array(datum_opt, field.data_type(), num_rows)?;
1941 columns.push((name, array));
1942 } else {
1943 if source_col_idx >= batch.num_columns() {
1944 return Err(DataFusionError::Plan(format!(
1945 "Source query has fewer columns than expected non-partition columns. \
1946 Expected column '{name}' at position {source_col_idx}"
1947 )));
1948 }
1949 let col = batch.column(source_col_idx).clone();
1950 let target_type = paimon::arrow::paimon_type_to_arrow(field.data_type())
1951 .map_err(to_datafusion_error)?;
1952 let col = if col.data_type() != &target_type {
1953 cast(&col, &target_type).map_err(|e| {
1954 DataFusionError::Plan(format!(
1955 "Cannot cast column '{name}' from {:?} to {:?}: {e}",
1956 col.data_type(),
1957 target_type
1958 ))
1959 })?
1960 } else {
1961 col
1962 };
1963 columns.push((name, col));
1964 source_col_idx += 1;
1965 }
1966 }
1967
1968 if source_col_idx != batch.num_columns() || source_col_idx != expected_source_cols {
1969 return Err(DataFusionError::Plan(format!(
1970 "Source query has {} columns, but expected {} non-partition columns",
1971 batch.num_columns(),
1972 expected_source_cols
1973 )));
1974 }
1975
1976 let fields: Vec<Field> = columns
1977 .iter()
1978 .map(|(name, arr)| Field::new(name, arr.data_type().clone(), true))
1979 .collect();
1980 let schema = Arc::new(Schema::new(fields));
1981 let arrays: Vec<ArrayRef> = columns.into_iter().map(|(_, arr)| arr).collect();
1982 RecordBatch::try_new(schema, arrays).map_err(|e| DataFusionError::ArrowError(Box::new(e), None))
1983}
1984
1985fn datum_to_constant_array(
1988 datum: &Option<Datum>,
1989 data_type: &PaimonDataType,
1990 num_rows: usize,
1991) -> DFResult<ArrayRef> {
1992 match datum {
1993 None => {
1994 let arrow_type =
1995 paimon::arrow::paimon_type_to_arrow(data_type).map_err(to_datafusion_error)?;
1996 Ok(new_null_array(&arrow_type, num_rows))
1997 }
1998 Some(d) => match d {
1999 Datum::Bool(v) => Ok(Arc::new(BooleanArray::from(vec![*v; num_rows]))),
2000 Datum::TinyInt(v) => Ok(Arc::new(Int8Array::from(vec![*v; num_rows]))),
2001 Datum::SmallInt(v) => Ok(Arc::new(Int16Array::from(vec![*v; num_rows]))),
2002 Datum::Int(v) => Ok(Arc::new(Int32Array::from(vec![*v; num_rows]))),
2003 Datum::Long(v) => Ok(Arc::new(Int64Array::from(vec![*v; num_rows]))),
2004 Datum::Float(v) => Ok(Arc::new(Float32Array::from(vec![*v; num_rows]))),
2005 Datum::Double(v) => Ok(Arc::new(Float64Array::from(vec![*v; num_rows]))),
2006 Datum::String(v) => Ok(Arc::new(StringArray::from(vec![v.as_str(); num_rows]))),
2007 Datum::Date(v) => Ok(Arc::new(Date32Array::from(vec![*v; num_rows]))),
2008 Datum::Time(_)
2009 | Datum::Timestamp { .. }
2010 | Datum::LocalZonedTimestamp { .. }
2011 | Datum::Decimal { .. }
2012 | Datum::Bytes(_) => Err(DataFusionError::Plan(format!(
2013 "Unsupported datum type for partition column: {d}"
2014 ))),
2015 },
2016 }
2017}
2018
2019struct VersionAsOfInfo {
2020 table_name: String,
2021 version: String,
2022 clause_range: (usize, usize),
2024}
2025
2026struct TimestampAsOfInfo {
2027 table_name: String,
2028 timestamp: String,
2029 clause_range: (usize, usize),
2031}
2032
2033fn contains_time_travel_keyword(sql: &str) -> bool {
2037 let lower = sql.to_lowercase();
2038 let bytes = lower.as_bytes();
2039 let len = bytes.len();
2040 let mut i = 0;
2041 while i < len {
2042 match bytes[i] {
2043 b'\'' => {
2044 i += 1;
2046 while i < len {
2047 if bytes[i] == b'\'' {
2048 i += 1;
2049 if i < len && bytes[i] == b'\'' {
2050 i += 1; } else {
2052 break;
2053 }
2054 } else {
2055 i += 1;
2056 }
2057 }
2058 }
2059 b'-' if i + 1 < len && bytes[i + 1] == b'-' => {
2060 i += 2;
2062 while i < len && bytes[i] != b'\n' {
2063 i += 1;
2064 }
2065 }
2066 b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
2067 i += 2;
2069 while i + 1 < len {
2070 if bytes[i] == b'*' && bytes[i + 1] == b'/' {
2071 i += 2;
2072 break;
2073 }
2074 i += 1;
2075 }
2076 }
2077 _ => {
2078 if i + 14 <= len && bytes[i..i + 14].eq_ignore_ascii_case(b"version as of ") {
2080 return true;
2081 }
2082 if i + 16 <= len && bytes[i..i + 16].eq_ignore_ascii_case(b"timestamp as of ") {
2083 return true;
2084 }
2085 i += 1;
2086 }
2087 }
2088 }
2089 false
2090}
2091
2092fn extract_all_version_as_of(sql: &str) -> Vec<VersionAsOfInfo> {
2095 let lower = sql.to_lowercase();
2096 let bytes = lower.as_bytes();
2097 let len = bytes.len();
2098 let sql_bytes = sql.as_bytes();
2099 let mut i = 0;
2100 let mut results = Vec::new();
2101
2102 while i < len {
2103 match bytes[i] {
2104 b'\'' => {
2105 i += 1;
2107 while i < len {
2108 if sql_bytes[i] == b'\'' {
2109 i += 1;
2110 if i < len && sql_bytes[i] == b'\'' {
2111 i += 1; } else {
2113 break;
2114 }
2115 } else {
2116 i += 1;
2117 }
2118 }
2119 }
2120 b'-' if i + 1 < len && bytes[i + 1] == b'-' => {
2121 i += 2;
2123 while i < len && bytes[i] != b'\n' {
2124 i += 1;
2125 }
2126 }
2127 b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
2128 i += 2;
2130 while i + 1 < len {
2131 if bytes[i] == b'*' && bytes[i + 1] == b'/' {
2132 i += 2;
2133 break;
2134 }
2135 i += 1;
2136 }
2137 }
2138 _ => {
2139 if i + 14 <= len && bytes[i..i + 14].eq_ignore_ascii_case(b"version as of ") {
2140 let kw_start = i;
2141 let val_start = i + 14;
2142 let remaining = &sql[val_start..];
2143
2144 let version = if let Some(after_quote) = remaining.strip_prefix('\'') {
2146 if let Some(close_quote) = after_quote.find('\'') {
2148 after_quote[..close_quote].to_string()
2149 } else {
2150 i += 1;
2151 continue;
2152 }
2153 } else {
2154 let v: String = remaining
2156 .chars()
2157 .take_while(|c| c.is_ascii_digit())
2158 .collect();
2159 if v.is_empty() {
2160 i += 1;
2161 continue;
2162 }
2163 v
2164 };
2165
2166 let is_quoted = remaining.starts_with('\'');
2167 let val_end = if is_quoted {
2168 val_start + version.len() + 2 } else {
2170 val_start + version.len()
2171 };
2172
2173 let table_end = sql[..kw_start].trim_end_matches(' ').len();
2175 let table_start = sql[..table_end]
2176 .rfind(|c: char| c.is_whitespace() || c == ',' || c == '(')
2177 .map(|idx| idx + 1)
2178 .unwrap_or(0);
2179 let table_name = sql[table_start..table_end].to_string();
2180
2181 if !table_name.is_empty() {
2182 results.push(VersionAsOfInfo {
2183 table_name,
2184 version,
2185 clause_range: (table_start, val_end),
2186 });
2187 }
2188
2189 i = val_end;
2190 } else {
2191 i += 1;
2192 }
2193 }
2194 }
2195 }
2196
2197 results
2198}
2199
2200fn extract_all_timestamp_as_of(sql: &str) -> Vec<TimestampAsOfInfo> {
2203 let lower = sql.to_lowercase();
2204 let bytes = lower.as_bytes();
2205 let len = bytes.len();
2206 let sql_bytes = sql.as_bytes();
2207 let mut i = 0;
2208 let mut results = Vec::new();
2209
2210 while i < len {
2211 match bytes[i] {
2212 b'\'' => {
2213 i += 1;
2215 while i < len {
2216 if sql_bytes[i] == b'\'' {
2217 i += 1;
2218 if i < len && sql_bytes[i] == b'\'' {
2219 i += 1; } else {
2221 break;
2222 }
2223 } else {
2224 i += 1;
2225 }
2226 }
2227 }
2228 b'-' if i + 1 < len && bytes[i + 1] == b'-' => {
2229 i += 2;
2231 while i < len && bytes[i] != b'\n' {
2232 i += 1;
2233 }
2234 }
2235 b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
2236 i += 2;
2238 while i + 1 < len {
2239 if bytes[i] == b'*' && bytes[i + 1] == b'/' {
2240 i += 2;
2241 break;
2242 }
2243 i += 1;
2244 }
2245 }
2246 _ => {
2247 if i + 16 <= len && bytes[i..i + 16].eq_ignore_ascii_case(b"timestamp as of ") {
2248 let kw_start = i;
2249 let val_start = i + 16;
2250 let remaining = &sql[val_start..];
2251
2252 if !remaining.starts_with('\'') {
2254 i += 1;
2255 continue;
2256 }
2257 if let Some(close_quote) = remaining[1..].find('\'') {
2258 let timestamp = remaining[1..close_quote + 1].to_string();
2259 let val_end = val_start + close_quote + 2; let table_end = sql[..kw_start].trim_end_matches(' ').len();
2263 let table_start = sql[..table_end]
2264 .rfind(|c: char| c.is_whitespace() || c == ',' || c == '(')
2265 .map(|idx| idx + 1)
2266 .unwrap_or(0);
2267 let table_name = sql[table_start..table_end].to_string();
2268
2269 if !table_name.is_empty() {
2270 results.push(TimestampAsOfInfo {
2271 table_name,
2272 timestamp,
2273 clause_range: (table_start, val_end),
2274 });
2275 }
2276
2277 i = val_end;
2278 } else {
2279 i += 1;
2280 }
2281 } else {
2282 i += 1;
2283 }
2284 }
2285 }
2286 }
2287
2288 results
2289}
2290
2291fn ok_result(ctx: &SessionContext) -> DFResult<DataFrame> {
2293 let schema = Arc::new(Schema::new(vec![Field::new(
2294 "result",
2295 ArrowDataType::Utf8,
2296 false,
2297 )]));
2298 let batch = RecordBatch::try_new(
2299 schema.clone(),
2300 vec![Arc::new(StringArray::from(vec!["OK"]))],
2301 )?;
2302 let df = ctx.read_batch(batch)?;
2303 Ok(df)
2304}
2305
2306fn register_table_functions(
2310 ctx: &SessionContext,
2311 catalog: &Arc<dyn Catalog>,
2312 default_database: &str,
2313) {
2314 crate::vector_search::register_vector_search(ctx, Arc::clone(catalog), default_database);
2315 #[cfg(feature = "fulltext")]
2316 crate::full_text_search::register_full_text_search(ctx, Arc::clone(catalog), default_database);
2317}
2318
2319#[cfg(test)]
2320mod tests {
2321 use super::*;
2322 use std::collections::HashMap;
2323 use std::sync::Mutex;
2324
2325 use async_trait::async_trait;
2326 use paimon::catalog::Database;
2327 use paimon::spec::{DataType as PaimonDataType, Schema as PaimonSchema};
2328 use paimon::table::Table;
2329
2330 #[allow(clippy::enum_variant_names)]
2333 #[derive(Debug)]
2334 enum CatalogCall {
2335 CreateTable {
2336 identifier: Identifier,
2337 schema: PaimonSchema,
2338 ignore_if_exists: bool,
2339 },
2340 AlterTable {
2341 identifier: Identifier,
2342 changes: Vec<SchemaChange>,
2343 ignore_if_not_exists: bool,
2344 },
2345 RenameTable {
2346 from: Identifier,
2347 to: Identifier,
2348 ignore_if_not_exists: bool,
2349 },
2350 }
2351
2352 struct MockCatalog {
2353 calls: Mutex<Vec<CatalogCall>>,
2354 }
2355
2356 impl MockCatalog {
2357 fn new() -> Self {
2358 Self {
2359 calls: Mutex::new(Vec::new()),
2360 }
2361 }
2362
2363 fn take_calls(&self) -> Vec<CatalogCall> {
2364 std::mem::take(&mut *self.calls.lock().unwrap())
2365 }
2366 }
2367
2368 #[async_trait]
2369 impl Catalog for MockCatalog {
2370 async fn list_databases(&self) -> paimon::Result<Vec<String>> {
2371 Ok(vec![])
2372 }
2373 async fn create_database(
2374 &self,
2375 _name: &str,
2376 _ignore_if_exists: bool,
2377 _properties: HashMap<String, String>,
2378 ) -> paimon::Result<()> {
2379 Ok(())
2380 }
2381 async fn get_database(&self, _name: &str) -> paimon::Result<Database> {
2382 Err(paimon::Error::DatabaseNotExist {
2383 database: _name.to_string(),
2384 })
2385 }
2386 async fn drop_database(
2387 &self,
2388 _name: &str,
2389 _ignore_if_not_exists: bool,
2390 _cascade: bool,
2391 ) -> paimon::Result<()> {
2392 Ok(())
2393 }
2394 async fn get_table(&self, _identifier: &Identifier) -> paimon::Result<Table> {
2395 Err(paimon::Error::TableNotExist {
2396 full_name: _identifier.to_string(),
2397 })
2398 }
2399 async fn list_tables(&self, _database_name: &str) -> paimon::Result<Vec<String>> {
2400 Ok(vec![])
2401 }
2402 async fn create_table(
2403 &self,
2404 identifier: &Identifier,
2405 creation: PaimonSchema,
2406 ignore_if_exists: bool,
2407 ) -> paimon::Result<()> {
2408 self.calls.lock().unwrap().push(CatalogCall::CreateTable {
2409 identifier: identifier.clone(),
2410 schema: creation,
2411 ignore_if_exists,
2412 });
2413 Ok(())
2414 }
2415 async fn drop_table(
2416 &self,
2417 _identifier: &Identifier,
2418 _ignore_if_not_exists: bool,
2419 ) -> paimon::Result<()> {
2420 Ok(())
2421 }
2422 async fn rename_table(
2423 &self,
2424 from: &Identifier,
2425 to: &Identifier,
2426 ignore_if_not_exists: bool,
2427 ) -> paimon::Result<()> {
2428 self.calls.lock().unwrap().push(CatalogCall::RenameTable {
2429 from: from.clone(),
2430 to: to.clone(),
2431 ignore_if_not_exists,
2432 });
2433 Ok(())
2434 }
2435 async fn alter_table(
2436 &self,
2437 identifier: &Identifier,
2438 changes: Vec<SchemaChange>,
2439 ignore_if_not_exists: bool,
2440 ) -> paimon::Result<()> {
2441 self.calls.lock().unwrap().push(CatalogCall::AlterTable {
2442 identifier: identifier.clone(),
2443 changes,
2444 ignore_if_not_exists,
2445 });
2446 Ok(())
2447 }
2448 }
2449
2450 async fn make_sql_context(catalog: Arc<MockCatalog>) -> SQLContext {
2451 let mut ctx = SQLContext::new();
2452 ctx.register_catalog("paimon", catalog).await.unwrap();
2453 ctx
2454 }
2455
2456 fn assert_sql_type_to_paimon(
2457 sql_type: datafusion::sql::sqlparser::ast::DataType,
2458 expected: PaimonDataType,
2459 ) {
2460 assert_eq!(
2461 sql_data_type_to_paimon_type(&sql_type, true).unwrap(),
2462 expected
2463 );
2464 }
2465
2466 #[test]
2469 fn test_sql_type_boolean() {
2470 use datafusion::sql::sqlparser::ast::DataType as SqlType;
2471 assert_sql_type_to_paimon(
2472 SqlType::Boolean,
2473 PaimonDataType::Boolean(BooleanType::new()),
2474 );
2475 }
2476
2477 #[test]
2478 fn test_sql_type_integers() {
2479 use datafusion::sql::sqlparser::ast::DataType as SqlType;
2480 assert_sql_type_to_paimon(
2481 SqlType::TinyInt(None),
2482 PaimonDataType::TinyInt(TinyIntType::new()),
2483 );
2484 assert_sql_type_to_paimon(
2485 SqlType::SmallInt(None),
2486 PaimonDataType::SmallInt(SmallIntType::new()),
2487 );
2488 assert_sql_type_to_paimon(SqlType::Int(None), PaimonDataType::Int(IntType::new()));
2489 assert_sql_type_to_paimon(SqlType::Integer(None), PaimonDataType::Int(IntType::new()));
2490 assert_sql_type_to_paimon(
2491 SqlType::BigInt(None),
2492 PaimonDataType::BigInt(BigIntType::new()),
2493 );
2494 }
2495
2496 #[test]
2497 fn test_sql_type_floats() {
2498 use datafusion::sql::sqlparser::ast::{DataType as SqlType, ExactNumberInfo};
2499 assert_sql_type_to_paimon(
2500 SqlType::Float(ExactNumberInfo::None),
2501 PaimonDataType::Float(FloatType::new()),
2502 );
2503 assert_sql_type_to_paimon(SqlType::Real, PaimonDataType::Float(FloatType::new()));
2504 assert_sql_type_to_paimon(
2505 SqlType::DoublePrecision,
2506 PaimonDataType::Double(DoubleType::new()),
2507 );
2508 }
2509
2510 #[test]
2511 fn test_sql_type_string_variants() {
2512 use datafusion::sql::sqlparser::ast::DataType as SqlType;
2513 for sql_type in [SqlType::Varchar(None), SqlType::Text, SqlType::String(None)] {
2514 assert_sql_type_to_paimon(
2515 sql_type.clone(),
2516 PaimonDataType::VarChar(
2517 VarCharType::with_nullable(true, VarCharType::MAX_LENGTH).unwrap(),
2518 ),
2519 );
2520 }
2521 }
2522
2523 #[test]
2524 fn test_sql_type_binary() {
2525 use datafusion::sql::sqlparser::ast::DataType as SqlType;
2526 assert_sql_type_to_paimon(
2527 SqlType::Bytea,
2528 PaimonDataType::VarBinary(
2529 VarBinaryType::try_new(true, VarBinaryType::MAX_LENGTH).unwrap(),
2530 ),
2531 );
2532 }
2533
2534 #[test]
2535 fn test_sql_type_date() {
2536 use datafusion::sql::sqlparser::ast::DataType as SqlType;
2537 assert_sql_type_to_paimon(SqlType::Date, PaimonDataType::Date(DateType::new()));
2538 }
2539
2540 #[test]
2541 fn test_sql_type_timestamp_default() {
2542 use datafusion::sql::sqlparser::ast::{DataType as SqlType, TimezoneInfo};
2543 assert_sql_type_to_paimon(
2544 SqlType::Timestamp(None, TimezoneInfo::None),
2545 PaimonDataType::Timestamp(TimestampType::with_nullable(true, 3).unwrap()),
2546 );
2547 }
2548
2549 #[test]
2550 fn test_sql_type_timestamp_with_precision() {
2551 use datafusion::sql::sqlparser::ast::{DataType as SqlType, TimezoneInfo};
2552 assert_sql_type_to_paimon(
2553 SqlType::Timestamp(Some(0), TimezoneInfo::None),
2554 PaimonDataType::Timestamp(TimestampType::with_nullable(true, 0).unwrap()),
2555 );
2556 assert_sql_type_to_paimon(
2557 SqlType::Timestamp(Some(3), TimezoneInfo::None),
2558 PaimonDataType::Timestamp(TimestampType::with_nullable(true, 3).unwrap()),
2559 );
2560 assert_sql_type_to_paimon(
2561 SqlType::Timestamp(Some(6), TimezoneInfo::None),
2562 PaimonDataType::Timestamp(TimestampType::with_nullable(true, 6).unwrap()),
2563 );
2564 assert_sql_type_to_paimon(
2565 SqlType::Timestamp(Some(9), TimezoneInfo::None),
2566 PaimonDataType::Timestamp(TimestampType::with_nullable(true, 9).unwrap()),
2567 );
2568 }
2569
2570 #[test]
2571 fn test_sql_type_timestamp_with_tz() {
2572 use datafusion::sql::sqlparser::ast::{DataType as SqlType, TimezoneInfo};
2573 assert_sql_type_to_paimon(
2574 SqlType::Timestamp(None, TimezoneInfo::WithTimeZone),
2575 PaimonDataType::LocalZonedTimestamp(
2576 LocalZonedTimestampType::with_nullable(true, 3).unwrap(),
2577 ),
2578 );
2579 }
2580
2581 #[test]
2582 fn test_sql_type_decimal() {
2583 use datafusion::sql::sqlparser::ast::{DataType as SqlType, ExactNumberInfo};
2584 assert_sql_type_to_paimon(
2585 SqlType::Decimal(ExactNumberInfo::PrecisionAndScale(18, 2)),
2586 PaimonDataType::Decimal(DecimalType::with_nullable(true, 18, 2).unwrap()),
2587 );
2588 assert_sql_type_to_paimon(
2589 SqlType::Decimal(ExactNumberInfo::Precision(10)),
2590 PaimonDataType::Decimal(DecimalType::with_nullable(true, 10, 0).unwrap()),
2591 );
2592 assert_sql_type_to_paimon(
2593 SqlType::Decimal(ExactNumberInfo::None),
2594 PaimonDataType::Decimal(DecimalType::with_nullable(true, 10, 0).unwrap()),
2595 );
2596 }
2597
2598 #[test]
2599 fn test_sql_type_unsupported() {
2600 use datafusion::sql::sqlparser::ast::DataType as SqlType;
2601 assert!(sql_data_type_to_paimon_type(&SqlType::Regclass, true).is_err());
2602 }
2603
2604 #[test]
2605 fn test_sql_type_array() {
2606 use datafusion::sql::sqlparser::ast::{ArrayElemTypeDef, DataType as SqlType};
2607 assert_sql_type_to_paimon(
2608 SqlType::Array(ArrayElemTypeDef::AngleBracket(Box::new(SqlType::Int(None)))),
2609 PaimonDataType::Array(PaimonArrayType::with_nullable(
2610 true,
2611 PaimonDataType::Int(IntType::new()),
2612 )),
2613 );
2614 }
2615
2616 #[test]
2617 fn test_sql_type_array_no_element() {
2618 use datafusion::sql::sqlparser::ast::{ArrayElemTypeDef, DataType as SqlType};
2619 assert!(
2620 sql_data_type_to_paimon_type(&SqlType::Array(ArrayElemTypeDef::None), true).is_err()
2621 );
2622 }
2623
2624 #[test]
2625 fn test_sql_type_map() {
2626 use datafusion::sql::sqlparser::ast::DataType as SqlType;
2627 assert_sql_type_to_paimon(
2628 SqlType::Map(
2629 Box::new(SqlType::Varchar(None)),
2630 Box::new(SqlType::Int(None)),
2631 ),
2632 PaimonDataType::Map(PaimonMapType::with_nullable(
2633 true,
2634 PaimonDataType::VarChar(
2635 VarCharType::with_nullable(false, VarCharType::MAX_LENGTH).unwrap(),
2636 ),
2637 PaimonDataType::Int(IntType::new()),
2638 )),
2639 );
2640 }
2641
2642 #[test]
2643 fn test_sql_type_struct() {
2644 use datafusion::sql::sqlparser::ast::{
2645 DataType as SqlType, Ident, StructBracketKind, StructField,
2646 };
2647 assert_sql_type_to_paimon(
2648 SqlType::Struct(
2649 vec![
2650 StructField {
2651 field_name: Some(Ident::new("name")),
2652 field_type: SqlType::Varchar(None),
2653 options: None,
2654 },
2655 StructField {
2656 field_name: Some(Ident::new("age")),
2657 field_type: SqlType::Int(None),
2658 options: None,
2659 },
2660 ],
2661 StructBracketKind::AngleBrackets,
2662 ),
2663 PaimonDataType::Row(PaimonRowType::with_nullable(
2664 true,
2665 vec![
2666 PaimonDataField::new(
2667 0,
2668 "name".to_string(),
2669 PaimonDataType::VarChar(
2670 VarCharType::with_nullable(true, VarCharType::MAX_LENGTH).unwrap(),
2671 ),
2672 ),
2673 PaimonDataField::new(1, "age".to_string(), PaimonDataType::Int(IntType::new())),
2674 ],
2675 )),
2676 );
2677 }
2678
2679 #[tokio::test]
2682 async fn test_resolve_three_part_name() {
2683 let catalog = Arc::new(MockCatalog::new());
2684 let sql_context = make_sql_context(catalog).await;
2685 let dialect = GenericDialect {};
2686 let stmts = Parser::parse_sql(&dialect, "SELECT * FROM paimon.mydb.mytable").unwrap();
2687 if let Statement::Query(q) = &stmts[0] {
2688 if let datafusion::sql::sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() {
2689 if let datafusion::sql::sqlparser::ast::TableFactor::Table { name, .. } =
2690 &sel.from[0].relation
2691 {
2692 let id = sql_context.resolve_table_name(name).unwrap();
2693 assert_eq!(id.database(), "mydb");
2694 assert_eq!(id.object(), "mytable");
2695 }
2696 }
2697 }
2698 }
2699
2700 #[tokio::test]
2701 async fn test_resolve_two_part_name() {
2702 let catalog = Arc::new(MockCatalog::new());
2703 let sql_context = make_sql_context(catalog).await;
2704 let dialect = GenericDialect {};
2705 let stmts = Parser::parse_sql(&dialect, "SELECT * FROM mydb.mytable").unwrap();
2706 if let Statement::Query(q) = &stmts[0] {
2707 if let datafusion::sql::sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() {
2708 if let datafusion::sql::sqlparser::ast::TableFactor::Table { name, .. } =
2709 &sel.from[0].relation
2710 {
2711 let id = sql_context.resolve_table_name(name).unwrap();
2712 assert_eq!(id.database(), "mydb");
2713 assert_eq!(id.object(), "mytable");
2714 }
2715 }
2716 }
2717 }
2718
2719 #[tokio::test]
2720 async fn test_resolve_wrong_catalog_name() {
2721 let catalog = Arc::new(MockCatalog::new());
2722 let sql_context = make_sql_context(catalog).await;
2723 let dialect = GenericDialect {};
2724 let stmts = Parser::parse_sql(&dialect, "SELECT * FROM other.mydb.mytable").unwrap();
2725 if let Statement::Query(q) = &stmts[0] {
2726 if let datafusion::sql::sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() {
2727 if let datafusion::sql::sqlparser::ast::TableFactor::Table { name, .. } =
2728 &sel.from[0].relation
2729 {
2730 let err = sql_context.resolve_table_name(name).unwrap_err();
2731 assert!(err.to_string().contains("Unknown catalog"));
2732 }
2733 }
2734 }
2735 }
2736
2737 #[tokio::test]
2738 async fn test_resolve_single_part_name_uses_default_schema() {
2739 let catalog = Arc::new(MockCatalog::new());
2740 let sql_context = make_sql_context(catalog).await;
2741 let dialect = GenericDialect {};
2742 let stmts = Parser::parse_sql(&dialect, "SELECT * FROM mytable").unwrap();
2743 if let Statement::Query(q) = &stmts[0] {
2744 if let datafusion::sql::sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() {
2745 if let datafusion::sql::sqlparser::ast::TableFactor::Table { name, .. } =
2746 &sel.from[0].relation
2747 {
2748 let id = sql_context.resolve_table_name(name).unwrap();
2749 assert_eq!(id.database(), "default");
2750 assert_eq!(id.object(), "mytable");
2751 }
2752 }
2753 }
2754 }
2755
2756 #[test]
2759 fn test_extract_options_none() {
2760 let opts = extract_options(&CreateTableOptions::None).unwrap();
2761 assert!(opts.is_empty());
2762 }
2763
2764 #[test]
2765 fn test_extract_options_with_kv() {
2766 let dialect = GenericDialect {};
2768 let stmts =
2769 Parser::parse_sql(&dialect, "CREATE TABLE t (id INT) WITH ('bucket' = '4')").unwrap();
2770 if let Statement::CreateTable(ct) = &stmts[0] {
2771 let opts = extract_options(&ct.table_options).unwrap();
2772 assert_eq!(opts.len(), 1);
2773 assert_eq!(opts[0].0, "bucket");
2774 assert_eq!(opts[0].1, "4");
2775 } else {
2776 panic!("expected CreateTable");
2777 }
2778 }
2779
2780 #[tokio::test]
2783 async fn test_create_table_basic() {
2784 let catalog = Arc::new(MockCatalog::new());
2785 let sql_context = make_sql_context(catalog.clone()).await;
2786
2787 sql_context
2788 .sql("CREATE TABLE mydb.t1 (id INT NOT NULL, name VARCHAR, PRIMARY KEY (id))")
2789 .await
2790 .unwrap();
2791
2792 let calls = catalog.take_calls();
2793 assert_eq!(calls.len(), 1);
2794 if let CatalogCall::CreateTable {
2795 identifier,
2796 schema,
2797 ignore_if_exists,
2798 } = &calls[0]
2799 {
2800 assert_eq!(identifier.database(), "mydb");
2801 assert_eq!(identifier.object(), "t1");
2802 assert!(!ignore_if_exists);
2803 assert_eq!(schema.primary_keys(), &["id"]);
2804 } else {
2805 panic!("expected CreateTable call");
2806 }
2807 }
2808
2809 #[tokio::test]
2810 async fn test_create_table_if_not_exists() {
2811 let catalog = Arc::new(MockCatalog::new());
2812 let sql_context = make_sql_context(catalog.clone()).await;
2813
2814 sql_context
2815 .sql("CREATE TABLE IF NOT EXISTS mydb.t1 (id INT)")
2816 .await
2817 .unwrap();
2818
2819 let calls = catalog.take_calls();
2820 assert_eq!(calls.len(), 1);
2821 if let CatalogCall::CreateTable {
2822 ignore_if_exists, ..
2823 } = &calls[0]
2824 {
2825 assert!(ignore_if_exists);
2826 } else {
2827 panic!("expected CreateTable call");
2828 }
2829 }
2830
2831 #[tokio::test]
2832 async fn test_create_table_with_options() {
2833 let catalog = Arc::new(MockCatalog::new());
2834 let sql_context = make_sql_context(catalog.clone()).await;
2835
2836 sql_context
2837 .sql("CREATE TABLE mydb.t1 (id INT) WITH ('bucket' = '4', 'file.format' = 'parquet')")
2838 .await
2839 .unwrap();
2840
2841 let calls = catalog.take_calls();
2842 assert_eq!(calls.len(), 1);
2843 if let CatalogCall::CreateTable { schema, .. } = &calls[0] {
2844 let opts = schema.options();
2845 assert_eq!(opts.get("bucket").unwrap(), "4");
2846 assert_eq!(opts.get("file.format").unwrap(), "parquet");
2847 } else {
2848 panic!("expected CreateTable call");
2849 }
2850 }
2851
2852 #[tokio::test]
2853 async fn test_create_table_three_part_name() {
2854 let catalog = Arc::new(MockCatalog::new());
2855 let sql_context = make_sql_context(catalog.clone()).await;
2856
2857 sql_context
2858 .sql("CREATE TABLE paimon.mydb.t1 (id INT)")
2859 .await
2860 .unwrap();
2861
2862 let calls = catalog.take_calls();
2863 if let CatalogCall::CreateTable { identifier, .. } = &calls[0] {
2864 assert_eq!(identifier.database(), "mydb");
2865 assert_eq!(identifier.object(), "t1");
2866 } else {
2867 panic!("expected CreateTable call");
2868 }
2869 }
2870
2871 #[tokio::test]
2872 async fn test_create_table_blob_type_preserved() {
2873 let catalog = Arc::new(MockCatalog::new());
2874 let sql_context = make_sql_context(catalog.clone()).await;
2875
2876 sql_context
2877 .sql("CREATE TABLE mydb.t1 (id INT, payload BLOB NOT NULL) WITH ('data-evolution.enabled' = 'true')")
2878 .await
2879 .unwrap();
2880
2881 let calls = catalog.take_calls();
2882 assert_eq!(calls.len(), 1);
2883 if let CatalogCall::CreateTable { schema, .. } = &calls[0] {
2884 assert_eq!(schema.fields().len(), 2);
2885 assert!(matches!(
2886 schema.fields()[1].data_type(),
2887 PaimonDataType::Blob(_)
2888 ));
2889 assert!(!schema.fields()[1].data_type().is_nullable());
2890 } else {
2891 panic!("expected CreateTable call");
2892 }
2893 }
2894
2895 #[tokio::test]
2896 async fn test_alter_table_add_column() {
2897 let catalog = Arc::new(MockCatalog::new());
2898 let sql_context = make_sql_context(catalog.clone()).await;
2899
2900 sql_context
2901 .sql("ALTER TABLE mydb.t1 ADD COLUMN age INT")
2902 .await
2903 .unwrap();
2904
2905 let calls = catalog.take_calls();
2906 assert_eq!(calls.len(), 1);
2907 if let CatalogCall::AlterTable {
2908 identifier,
2909 changes,
2910 ..
2911 } = &calls[0]
2912 {
2913 assert_eq!(identifier.database(), "mydb");
2914 assert_eq!(identifier.object(), "t1");
2915 assert_eq!(changes.len(), 1);
2916 assert!(
2917 matches!(&changes[0], SchemaChange::AddColumn { field_name, .. } if field_name == "age")
2918 );
2919 } else {
2920 panic!("expected AlterTable call");
2921 }
2922 }
2923
2924 #[tokio::test]
2925 async fn test_alter_table_add_blob_column() {
2926 let catalog = Arc::new(MockCatalog::new());
2927 let sql_context = make_sql_context(catalog.clone()).await;
2928
2929 sql_context
2930 .sql("ALTER TABLE mydb.t1 ADD COLUMN payload BLOB")
2931 .await
2932 .unwrap();
2933
2934 let calls = catalog.take_calls();
2935 assert_eq!(calls.len(), 1);
2936 if let CatalogCall::AlterTable { changes, .. } = &calls[0] {
2937 assert_eq!(changes.len(), 1);
2938 assert!(matches!(
2939 &changes[0],
2940 SchemaChange::AddColumn {
2941 field_name,
2942 data_type,
2943 ..
2944 } if field_name == "payload" && matches!(data_type, PaimonDataType::Blob(_))
2945 ));
2946 } else {
2947 panic!("expected AlterTable call");
2948 }
2949 }
2950
2951 #[tokio::test]
2952 async fn test_alter_table_drop_column() {
2953 let catalog = Arc::new(MockCatalog::new());
2954 let sql_context = make_sql_context(catalog.clone()).await;
2955
2956 sql_context
2957 .sql("ALTER TABLE mydb.t1 DROP COLUMN age")
2958 .await
2959 .unwrap();
2960
2961 let calls = catalog.take_calls();
2962 assert_eq!(calls.len(), 1);
2963 if let CatalogCall::AlterTable { changes, .. } = &calls[0] {
2964 assert_eq!(changes.len(), 1);
2965 assert!(
2966 matches!(&changes[0], SchemaChange::DropColumn { field_name } if field_name == "age")
2967 );
2968 } else {
2969 panic!("expected AlterTable call");
2970 }
2971 }
2972
2973 #[tokio::test]
2974 async fn test_alter_table_rename_column() {
2975 let catalog = Arc::new(MockCatalog::new());
2976 let sql_context = make_sql_context(catalog.clone()).await;
2977
2978 sql_context
2979 .sql("ALTER TABLE mydb.t1 RENAME COLUMN old_name TO new_name")
2980 .await
2981 .unwrap();
2982
2983 let calls = catalog.take_calls();
2984 assert_eq!(calls.len(), 1);
2985 if let CatalogCall::AlterTable { changes, .. } = &calls[0] {
2986 assert_eq!(changes.len(), 1);
2987 assert!(matches!(
2988 &changes[0],
2989 SchemaChange::RenameColumn { field_name, new_name }
2990 if field_name == "old_name" && new_name == "new_name"
2991 ));
2992 } else {
2993 panic!("expected AlterTable call");
2994 }
2995 }
2996
2997 #[tokio::test]
2998 async fn test_alter_table_rename_table() {
2999 let catalog = Arc::new(MockCatalog::new());
3000 let sql_context = make_sql_context(catalog.clone()).await;
3001
3002 sql_context
3003 .sql("ALTER TABLE mydb.t1 RENAME TO t2")
3004 .await
3005 .unwrap();
3006
3007 let calls = catalog.take_calls();
3008 assert_eq!(calls.len(), 1);
3009 if let CatalogCall::RenameTable { from, to, .. } = &calls[0] {
3010 assert_eq!(from.database(), "mydb");
3011 assert_eq!(from.object(), "t1");
3012 assert_eq!(to.database(), "mydb");
3013 assert_eq!(to.object(), "t2");
3014 } else {
3015 panic!("expected RenameTable call");
3016 }
3017 }
3018
3019 #[tokio::test]
3020 async fn test_alter_table_if_exists_add_column() {
3021 let catalog = Arc::new(MockCatalog::new());
3022 let sql_context = make_sql_context(catalog.clone()).await;
3023
3024 sql_context
3025 .sql("ALTER TABLE IF EXISTS mydb.t1 ADD COLUMN age INT")
3026 .await
3027 .unwrap();
3028
3029 let calls = catalog.take_calls();
3030 assert_eq!(calls.len(), 1);
3031 if let CatalogCall::AlterTable {
3032 ignore_if_not_exists,
3033 ..
3034 } = &calls[0]
3035 {
3036 assert!(ignore_if_not_exists);
3037 } else {
3038 panic!("expected AlterTable call");
3039 }
3040 }
3041
3042 #[tokio::test]
3043 async fn test_alter_table_without_if_exists() {
3044 let catalog = Arc::new(MockCatalog::new());
3045 let sql_context = make_sql_context(catalog.clone()).await;
3046
3047 sql_context
3048 .sql("ALTER TABLE mydb.t1 ADD COLUMN age INT")
3049 .await
3050 .unwrap();
3051
3052 let calls = catalog.take_calls();
3053 if let CatalogCall::AlterTable {
3054 ignore_if_not_exists,
3055 ..
3056 } = &calls[0]
3057 {
3058 assert!(!ignore_if_not_exists);
3059 } else {
3060 panic!("expected AlterTable call");
3061 }
3062 }
3063
3064 #[tokio::test]
3065 async fn test_alter_table_if_exists_rename() {
3066 let catalog = Arc::new(MockCatalog::new());
3067 let sql_context = make_sql_context(catalog.clone()).await;
3068
3069 sql_context
3070 .sql("ALTER TABLE IF EXISTS mydb.t1 RENAME TO t2")
3071 .await
3072 .unwrap();
3073
3074 let calls = catalog.take_calls();
3075 assert_eq!(calls.len(), 1);
3076 if let CatalogCall::RenameTable {
3077 from,
3078 to,
3079 ignore_if_not_exists,
3080 } = &calls[0]
3081 {
3082 assert!(ignore_if_not_exists);
3083 assert_eq!(from.object(), "t1");
3084 assert_eq!(to.object(), "t2");
3085 } else {
3086 panic!("expected RenameTable call");
3087 }
3088 }
3089
3090 #[tokio::test]
3091 async fn test_alter_table_rename_three_part_name() {
3092 let catalog = Arc::new(MockCatalog::new());
3093 let sql_context = make_sql_context(catalog.clone()).await;
3094
3095 sql_context
3096 .sql("ALTER TABLE paimon.mydb.t1 RENAME TO t2")
3097 .await
3098 .unwrap();
3099
3100 let calls = catalog.take_calls();
3101 assert_eq!(calls.len(), 1);
3102 if let CatalogCall::RenameTable { from, to, .. } = &calls[0] {
3103 assert_eq!(from.database(), "mydb");
3104 assert_eq!(from.object(), "t1");
3105 assert_eq!(to.database(), "mydb");
3106 assert_eq!(to.object(), "t2");
3107 } else {
3108 panic!("expected RenameTable call");
3109 }
3110 }
3111
3112 #[tokio::test]
3113 async fn test_sql_parse_error() {
3114 let catalog = Arc::new(MockCatalog::new());
3115 let sql_context = make_sql_context(catalog).await;
3116 let result = sql_context.sql("NOT VALID SQL !!!").await;
3117 assert!(result.is_err());
3118 assert!(result.unwrap_err().to_string().contains("SQL parse error"));
3119 }
3120
3121 #[tokio::test]
3122 async fn test_multiple_statements_error() {
3123 let catalog = Arc::new(MockCatalog::new());
3124 let sql_context = make_sql_context(catalog).await;
3125 let result = sql_context.sql("SELECT 1; SELECT 2").await;
3126 assert!(result.is_err());
3127 assert!(result
3128 .unwrap_err()
3129 .to_string()
3130 .contains("exactly one SQL statement"));
3131 }
3132
3133 #[tokio::test]
3134 async fn test_create_external_table_rejected() {
3135 let catalog = Arc::new(MockCatalog::new());
3136 let sql_context = make_sql_context(catalog).await;
3137 let result = sql_context
3138 .sql("CREATE EXTERNAL TABLE mydb.t1 (id INT) STORED AS PARQUET")
3139 .await;
3140 assert!(result.is_err());
3141 assert!(result
3142 .unwrap_err()
3143 .to_string()
3144 .contains("CREATE EXTERNAL TABLE is not supported"));
3145 }
3146
3147 #[tokio::test]
3148 async fn test_non_ddl_delegates_to_datafusion() {
3149 let catalog = Arc::new(MockCatalog::new());
3150 let sql_context = make_sql_context(catalog.clone()).await;
3151 let df = sql_context.sql("SELECT 1 AS x").await.unwrap();
3153 let batches = df.collect().await.unwrap();
3154 assert_eq!(batches.len(), 1);
3155 assert_eq!(batches[0].num_rows(), 1);
3156 assert!(catalog.take_calls().is_empty());
3158 }
3159
3160 #[test]
3163 fn test_extract_partition_by_no_clause() {
3164 let (rewritten, keys) = extract_partition_by("CREATE TABLE t (id INT)").unwrap();
3165 assert_eq!(rewritten, "CREATE TABLE t (id INT)");
3166 assert!(keys.is_empty());
3167 }
3168
3169 #[test]
3170 fn test_extract_partition_by_single_column() {
3171 let (rewritten, keys) = extract_partition_by(
3172 "CREATE TABLE t (id INT, dt STRING) PARTITIONED BY (dt) WITH ('k'='v')",
3173 )
3174 .unwrap();
3175 assert_eq!(keys, vec!["dt"]);
3176 assert!(!rewritten.contains("PARTITIONED"));
3177 assert!(rewritten.contains("WITH"));
3178 }
3179
3180 #[test]
3181 fn test_extract_partition_by_multiple_columns() {
3182 let (_, keys) =
3183 extract_partition_by("CREATE TABLE t (a INT, b INT, c INT) PARTITIONED BY (a, b)")
3184 .unwrap();
3185 assert_eq!(keys, vec!["a", "b"]);
3186 }
3187
3188 #[test]
3189 fn test_extract_partition_by_mixed_case() {
3190 let (_, keys) =
3191 extract_partition_by("CREATE TABLE t (dt INT) Partitioned by (dt)").unwrap();
3192 assert_eq!(keys, vec!["dt"]);
3193 }
3194
3195 #[test]
3196 fn test_extract_partition_by_rejects_typed_column() {
3197 let err = extract_partition_by("CREATE TABLE t (dt STRING) PARTITIONED BY (dt STRING)")
3198 .unwrap_err();
3199 assert!(err.to_string().contains("should not specify a type"));
3200 }
3201
3202 #[test]
3203 fn test_extract_partition_by_empty_parens() {
3204 let err = extract_partition_by("CREATE TABLE t (id INT) PARTITIONED BY ()").unwrap_err();
3205 assert!(err.to_string().contains("at least one column"));
3206 }
3207
3208 #[test]
3209 fn test_extract_partition_by_unmatched_paren() {
3210 let err = extract_partition_by("CREATE TABLE t (id INT) PARTITIONED BY (dt").unwrap_err();
3211 assert!(err.to_string().contains("Unmatched"));
3212 }
3213
3214 #[test]
3215 fn test_extract_partition_by_skips_string_literal() {
3216 let sql =
3217 "CREATE TABLE t (id INT) WITH ('note' = 'PARTITIONED BY (x)') PARTITIONED BY (id)";
3218 let (rewritten, keys) = extract_partition_by(sql).unwrap();
3219 assert_eq!(keys, vec!["id"]);
3220 assert!(rewritten.contains("WITH"));
3221 assert!(rewritten.contains("'PARTITIONED BY (x)'"));
3222 }
3223
3224 #[test]
3225 fn test_extract_partition_by_skips_line_comment() {
3226 let sql = "CREATE TABLE t (id INT) -- PARTITIONED BY (x)\nPARTITIONED BY (id)";
3227 let (_, keys) = extract_partition_by(sql).unwrap();
3228 assert_eq!(keys, vec!["id"]);
3229 }
3230
3231 #[test]
3232 fn test_extract_partition_by_double_quoted_identifier() {
3233 let (_, keys) =
3234 extract_partition_by("CREATE TABLE t (\"order\" INT) PARTITIONED BY (\"order\")")
3235 .unwrap();
3236 assert_eq!(keys, vec!["order"]);
3237 }
3238
3239 #[test]
3240 fn test_extract_partition_by_backtick_quoted_identifier() {
3241 let (_, keys) =
3242 extract_partition_by("CREATE TABLE t (`order` INT) PARTITIONED BY (`order`)").unwrap();
3243 assert_eq!(keys, vec!["order"]);
3244 }
3245
3246 #[test]
3247 fn test_extract_partition_by_no_paren_after_by() {
3248 let err = extract_partition_by("CREATE TABLE t (id INT) PARTITIONED BY dt").unwrap_err();
3249 assert!(err.to_string().contains("Expected '('"));
3250 }
3251
3252 #[test]
3253 fn test_extract_partition_by_only_partitioned_no_by() {
3254 let (rewritten, keys) = extract_partition_by("CREATE TABLE partitioned (id INT)").unwrap();
3255 assert_eq!(rewritten, "CREATE TABLE partitioned (id INT)");
3256 assert!(keys.is_empty());
3257 }
3258
3259 #[test]
3260 fn test_extract_partition_by_skips_block_comment() {
3261 let sql = "CREATE TABLE t (id INT) /* PARTITIONED BY (x) */ PARTITIONED BY (id)";
3262 let (rewritten, keys) = extract_partition_by(sql).unwrap();
3263 assert_eq!(keys, vec!["id"]);
3264 assert!(rewritten.contains("/* PARTITIONED BY (x) */"));
3265 }
3266
3267 #[test]
3268 fn test_looks_like_create_table() {
3269 assert!(looks_like_create_table("CREATE TABLE t (id INT)"));
3270 assert!(looks_like_create_table(" create table t (id INT)"));
3271 assert!(looks_like_create_table(
3272 "CREATE TABLE IF NOT EXISTS t (id INT)",
3273 ));
3274 assert!(looks_like_create_table(
3275 "/* note */ CREATE TABLE t (id INT)",
3276 ));
3277 assert!(looks_like_create_table(
3278 "-- comment\nCREATE TABLE t (id INT)",
3279 ));
3280 assert!(looks_like_create_table(
3281 "/* a */ /* b */ CREATE TABLE t (id INT)",
3282 ));
3283 assert!(!looks_like_create_table("ALTER TABLE t ADD COLUMN x INT"));
3284 assert!(!looks_like_create_table("SELECT 1"));
3285 assert!(!looks_like_create_table(
3286 "SELECT aaaaaaaaaaaaaaaaaaaa中文 FROM t",
3287 ));
3288 }
3289
3290 #[tokio::test]
3293 async fn test_create_table_partition_key_not_in_columns() {
3294 let catalog = Arc::new(MockCatalog::new());
3295 let sql_context = make_sql_context(catalog).await;
3296 let err = sql_context
3297 .sql("CREATE TABLE mydb.t (id INT, dt STRING) PARTITIONED BY (nonexistent)")
3298 .await
3299 .unwrap_err();
3300 assert!(err.to_string().contains("is not defined in the table"));
3301 }
3302
3303 #[tokio::test]
3304 async fn test_create_table_partition_key_matches_column() {
3305 let catalog = Arc::new(MockCatalog::new());
3306 let sql_context = make_sql_context(catalog.clone()).await;
3307 sql_context
3308 .sql("CREATE TABLE mydb.t (id INT, dt STRING) PARTITIONED BY (dt)")
3309 .await
3310 .unwrap();
3311 let calls = catalog.take_calls();
3312 assert_eq!(calls.len(), 1);
3313 if let CatalogCall::CreateTable { schema, .. } = &calls[0] {
3314 assert_eq!(schema.partition_keys(), &["dt"]);
3315 } else {
3316 panic!("expected CreateTable call");
3317 }
3318 }
3319
3320 #[tokio::test]
3323 async fn test_set_paimon_option() {
3324 let catalog = Arc::new(MockCatalog::new());
3325 let sql_context = make_sql_context(catalog).await;
3326 sql_context
3327 .sql("SET 'paimon.scan.version' = '1'")
3328 .await
3329 .unwrap();
3330 let opts = sql_context.dynamic_options().read().unwrap();
3331 assert_eq!(opts.get("scan.version").unwrap(), "1");
3332 }
3333
3334 #[tokio::test]
3335 async fn test_set_paimon_option_overwrites() {
3336 let catalog = Arc::new(MockCatalog::new());
3337 let sql_context = make_sql_context(catalog).await;
3338 sql_context
3339 .sql("SET 'paimon.scan.version' = '1'")
3340 .await
3341 .unwrap();
3342 sql_context
3343 .sql("SET 'paimon.scan.version' = '2'")
3344 .await
3345 .unwrap();
3346 let opts = sql_context.dynamic_options().read().unwrap();
3347 assert_eq!(opts.get("scan.version").unwrap(), "2");
3348 }
3349
3350 #[tokio::test]
3351 async fn test_reset_paimon_option() {
3352 let catalog = Arc::new(MockCatalog::new());
3353 let sql_context = make_sql_context(catalog).await;
3354 sql_context
3355 .sql("SET 'paimon.scan.version' = '1'")
3356 .await
3357 .unwrap();
3358 sql_context
3359 .sql("RESET 'paimon.scan.version'")
3360 .await
3361 .unwrap();
3362 let opts = sql_context.dynamic_options().read().unwrap();
3363 assert!(opts.get("scan.version").is_none());
3364 }
3365
3366 #[tokio::test]
3367 async fn test_set_non_paimon_option_delegates() {
3368 let catalog = Arc::new(MockCatalog::new());
3369 let sql_context = make_sql_context(catalog).await;
3370 let _ = sql_context
3373 .sql("SET datafusion.optimizer.max_passes = 3")
3374 .await;
3375 let opts = sql_context.dynamic_options().read().unwrap();
3376 assert!(opts.is_empty());
3377 }
3378
3379 #[tokio::test]
3380 async fn test_set_multiple_paimon_options() {
3381 let catalog = Arc::new(MockCatalog::new());
3382 let sql_context = make_sql_context(catalog).await;
3383 sql_context
3384 .sql("SET 'paimon.scan.version' = '1'")
3385 .await
3386 .unwrap();
3387 sql_context
3388 .sql("SET 'paimon.scan.timestamp-millis' = '1000'")
3389 .await
3390 .unwrap();
3391 let opts = sql_context.dynamic_options().read().unwrap();
3392 assert_eq!(opts.len(), 2);
3393 assert_eq!(opts.get("scan.version").unwrap(), "1");
3394 assert_eq!(opts.get("scan.timestamp-millis").unwrap(), "1000");
3395 }
3396
3397 #[tokio::test]
3398 async fn test_reset_nonexistent_paimon_option_is_noop() {
3399 let catalog = Arc::new(MockCatalog::new());
3400 let sql_context = make_sql_context(catalog).await;
3401 sql_context
3402 .sql("RESET 'paimon.scan.version'")
3403 .await
3404 .unwrap();
3405 let opts = sql_context.dynamic_options().read().unwrap();
3406 assert!(opts.is_empty());
3407 }
3408
3409 async fn setup_fs_sql_context() -> (tempfile::TempDir, SQLContext) {
3412 use paimon::{CatalogOptions, FileSystemCatalog, Options};
3413
3414 let temp_dir = tempfile::TempDir::new().unwrap();
3415 let warehouse = format!("file://{}", temp_dir.path().display());
3416 let mut options = Options::new();
3417 options.set(CatalogOptions::WAREHOUSE, warehouse);
3418 let catalog = Arc::new(FileSystemCatalog::new(options).unwrap());
3419
3420 let mut sql_context = SQLContext::new();
3421 sql_context
3422 .register_catalog("paimon", catalog.clone())
3423 .await
3424 .unwrap();
3425 sql_context
3426 .sql("CREATE SCHEMA paimon.test_db")
3427 .await
3428 .unwrap();
3429
3430 (temp_dir, sql_context)
3431 }
3432
3433 #[tokio::test]
3434 async fn test_truncate_table() {
3435 let (_tmp, sql_context) = setup_fs_sql_context().await;
3436
3437 sql_context
3438 .sql("CREATE TABLE paimon.test_db.t1 (id INT, value INT)")
3439 .await
3440 .unwrap();
3441 sql_context
3442 .sql("INSERT INTO paimon.test_db.t1 VALUES (1, 10), (2, 20)")
3443 .await
3444 .unwrap()
3445 .collect()
3446 .await
3447 .unwrap();
3448
3449 sql_context
3450 .sql("TRUNCATE TABLE paimon.test_db.t1")
3451 .await
3452 .unwrap();
3453
3454 let batches = sql_context
3455 .sql("SELECT * FROM paimon.test_db.t1")
3456 .await
3457 .unwrap()
3458 .collect()
3459 .await
3460 .unwrap();
3461 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
3462 assert_eq!(total, 0);
3463 }
3464
3465 #[tokio::test]
3466 async fn test_truncate_table_partition() {
3467 let (_tmp, sql_context) = setup_fs_sql_context().await;
3468
3469 sql_context
3470 .sql("CREATE TABLE paimon.test_db.t2 (pt VARCHAR, id INT) PARTITIONED BY (pt)")
3471 .await
3472 .unwrap();
3473 sql_context
3474 .sql("INSERT INTO paimon.test_db.t2 VALUES ('a', 1), ('a', 2), ('b', 3), ('b', 4)")
3475 .await
3476 .unwrap()
3477 .collect()
3478 .await
3479 .unwrap();
3480
3481 sql_context
3482 .sql("TRUNCATE TABLE paimon.test_db.t2 PARTITION (pt = 'a')")
3483 .await
3484 .unwrap();
3485
3486 let batches = sql_context
3487 .sql("SELECT pt, id FROM paimon.test_db.t2 ORDER BY id")
3488 .await
3489 .unwrap()
3490 .collect()
3491 .await
3492 .unwrap();
3493
3494 let mut rows = Vec::new();
3495 for batch in &batches {
3496 let pts = batch
3497 .column(0)
3498 .as_any()
3499 .downcast_ref::<StringArray>()
3500 .unwrap();
3501 let ids = batch
3502 .column(1)
3503 .as_any()
3504 .downcast_ref::<Int32Array>()
3505 .unwrap();
3506 for i in 0..batch.num_rows() {
3507 rows.push((pts.value(i).to_string(), ids.value(i)));
3508 }
3509 }
3510 assert_eq!(rows, vec![("b".to_string(), 3), ("b".to_string(), 4)]);
3511 }
3512
3513 #[tokio::test]
3514 async fn test_alter_table_drop_partitions() {
3515 let (_tmp, sql_context) = setup_fs_sql_context().await;
3516
3517 sql_context
3518 .sql("CREATE TABLE paimon.test_db.t3 (pt VARCHAR, id INT) PARTITIONED BY (pt)")
3519 .await
3520 .unwrap();
3521 sql_context
3522 .sql("INSERT INTO paimon.test_db.t3 VALUES ('a', 1), ('a', 2), ('b', 3), ('b', 4)")
3523 .await
3524 .unwrap()
3525 .collect()
3526 .await
3527 .unwrap();
3528
3529 sql_context
3530 .sql("ALTER TABLE paimon.test_db.t3 DROP PARTITION (pt = 'b')")
3531 .await
3532 .unwrap();
3533
3534 let batches = sql_context
3535 .sql("SELECT pt, id FROM paimon.test_db.t3 ORDER BY id")
3536 .await
3537 .unwrap()
3538 .collect()
3539 .await
3540 .unwrap();
3541
3542 let mut rows = Vec::new();
3543 for batch in &batches {
3544 let pts = batch
3545 .column(0)
3546 .as_any()
3547 .downcast_ref::<StringArray>()
3548 .unwrap();
3549 let ids = batch
3550 .column(1)
3551 .as_any()
3552 .downcast_ref::<Int32Array>()
3553 .unwrap();
3554 for i in 0..batch.num_rows() {
3555 rows.push((pts.value(i).to_string(), ids.value(i)));
3556 }
3557 }
3558 assert_eq!(rows, vec![("a".to_string(), 1), ("a".to_string(), 2)]);
3559 }
3560
3561 #[tokio::test]
3562 async fn test_truncate_table_incomplete_partition_spec() {
3563 let (_tmp, sql_context) = setup_fs_sql_context().await;
3564
3565 sql_context
3566 .sql("CREATE TABLE paimon.test_db.t_multi (pt1 VARCHAR, pt2 VARCHAR, id INT) PARTITIONED BY (pt1, pt2)")
3567 .await
3568 .unwrap();
3569 sql_context
3570 .sql("INSERT INTO paimon.test_db.t_multi VALUES ('a', 'x', 1)")
3571 .await
3572 .unwrap()
3573 .collect()
3574 .await
3575 .unwrap();
3576
3577 let err = sql_context
3578 .sql("TRUNCATE TABLE paimon.test_db.t_multi PARTITION (pt1 = 'a')")
3579 .await
3580 .unwrap_err();
3581 assert!(
3582 err.to_string().contains("Incomplete partition spec"),
3583 "Expected incomplete partition spec error, got: {err}"
3584 );
3585 }
3586
3587 #[tokio::test]
3588 async fn test_truncate_table_if_exists_nonexistent() {
3589 let (_tmp, sql_context) = setup_fs_sql_context().await;
3590
3591 sql_context
3592 .sql("TRUNCATE TABLE IF EXISTS paimon.test_db.nonexistent")
3593 .await
3594 .unwrap();
3595 }
3596
3597 #[tokio::test]
3598 async fn test_truncate_table_nonexistent_without_if_exists() {
3599 let (_tmp, sql_context) = setup_fs_sql_context().await;
3600
3601 let err = sql_context
3602 .sql("TRUNCATE TABLE paimon.test_db.nonexistent")
3603 .await
3604 .unwrap_err();
3605 assert!(
3606 err.to_string().contains("does not exist"),
3607 "Expected table-not-exist error, got: {err}"
3608 );
3609 }
3610
3611 #[tokio::test]
3612 async fn test_alter_table_if_exists_drop_partition_nonexistent() {
3613 let (_tmp, sql_context) = setup_fs_sql_context().await;
3614
3615 sql_context
3616 .sql("ALTER TABLE IF EXISTS paimon.test_db.nonexistent DROP PARTITION (pt = 'a')")
3617 .await
3618 .unwrap();
3619 }
3620
3621 #[tokio::test]
3622 async fn test_drop_partition_incomplete_spec() {
3623 let (_tmp, sql_context) = setup_fs_sql_context().await;
3624
3625 sql_context
3626 .sql("CREATE TABLE paimon.test_db.t_dp (pt1 VARCHAR, pt2 VARCHAR, id INT) PARTITIONED BY (pt1, pt2)")
3627 .await
3628 .unwrap();
3629 sql_context
3630 .sql("INSERT INTO paimon.test_db.t_dp VALUES ('a', 'x', 1)")
3631 .await
3632 .unwrap()
3633 .collect()
3634 .await
3635 .unwrap();
3636
3637 let err = sql_context
3638 .sql("ALTER TABLE paimon.test_db.t_dp DROP PARTITION (pt1 = 'a')")
3639 .await
3640 .unwrap_err();
3641 assert!(
3642 err.to_string().contains("Incomplete partition spec"),
3643 "Expected incomplete partition spec error, got: {err}"
3644 );
3645 }
3646
3647 #[tokio::test]
3648 async fn test_create_temp_table_if_not_exists() {
3649 let catalog = Arc::new(MockCatalog::new());
3650 let sql_context = make_sql_context(catalog).await;
3651
3652 sql_context
3654 .sql("CREATE TEMPORARY TABLE mydb.t1 (id INT)")
3655 .await
3656 .unwrap();
3657
3658 let err = sql_context
3660 .sql("CREATE TEMPORARY TABLE mydb.t1 (id INT)")
3661 .await
3662 .unwrap_err();
3663 assert!(
3664 err.to_string().contains("already exists"),
3665 "Expected already-exists error, got: {err}"
3666 );
3667
3668 sql_context
3670 .sql("CREATE TEMPORARY TABLE IF NOT EXISTS mydb.t1 (id INT)")
3671 .await
3672 .unwrap();
3673 }
3674
3675 #[tokio::test]
3676 async fn test_create_temp_table_if_not_exists_as_select() {
3677 let catalog = Arc::new(MockCatalog::new());
3678 let sql_context = make_sql_context(catalog).await;
3679
3680 sql_context
3682 .sql("CREATE TEMPORARY TABLE mydb.t2 AS SELECT 1 AS id")
3683 .await
3684 .unwrap();
3685
3686 sql_context
3688 .sql("CREATE TEMPORARY TABLE IF NOT EXISTS mydb.t2 AS SELECT 2 AS id")
3689 .await
3690 .unwrap();
3691
3692 let df = sql_context.sql("SELECT * FROM mydb.t2").await.unwrap();
3694 let batches = df.collect().await.unwrap();
3695 let val = batches[0]
3696 .column(0)
3697 .as_any()
3698 .downcast_ref::<Int64Array>()
3699 .unwrap();
3700 assert_eq!(val.value(0), 1);
3701 }
3702
3703 #[tokio::test]
3704 async fn test_create_temp_view_if_not_exists() {
3705 let catalog = Arc::new(MockCatalog::new());
3706 let sql_context = make_sql_context(catalog).await;
3707
3708 sql_context
3710 .sql("CREATE TEMPORARY VIEW mydb.v1 AS SELECT 1 AS id")
3711 .await
3712 .unwrap();
3713
3714 let err = sql_context
3716 .sql("CREATE TEMPORARY VIEW mydb.v1 AS SELECT 2 AS id")
3717 .await
3718 .unwrap_err();
3719 assert!(
3720 err.to_string().contains("already exists"),
3721 "Expected already-exists error, got: {err}"
3722 );
3723
3724 sql_context
3726 .sql("CREATE TEMPORARY VIEW IF NOT EXISTS mydb.v1 AS SELECT 3 AS id")
3727 .await
3728 .unwrap();
3729
3730 let df = sql_context.sql("SELECT * FROM mydb.v1").await.unwrap();
3732 let batches = df.collect().await.unwrap();
3733 let val = batches[0]
3734 .column(0)
3735 .as_any()
3736 .downcast_ref::<Int64Array>()
3737 .unwrap();
3738 assert_eq!(val.value(0), 1);
3739 }
3740
3741 #[tokio::test]
3742 async fn test_drop_temp_table_if_exists() {
3743 let catalog = Arc::new(MockCatalog::new());
3744 let sql_context = make_sql_context(catalog).await;
3745
3746 let err = sql_context
3748 .sql("DROP TEMPORARY TABLE mydb.nonexistent")
3749 .await
3750 .unwrap_err();
3751 let msg = err.to_string();
3752 assert!(
3753 msg.contains("doesn't exist")
3754 || msg.contains("does not exist")
3755 || msg.contains("Unknown temp database"),
3756 "Expected table-not-exist error, got: {msg}"
3757 );
3758
3759 sql_context
3761 .sql("DROP TEMPORARY TABLE IF EXISTS mydb.nonexistent")
3762 .await
3763 .unwrap();
3764
3765 sql_context
3767 .sql("CREATE TEMPORARY TABLE mydb.t1 (id INT)")
3768 .await
3769 .unwrap();
3770
3771 sql_context
3772 .sql("DROP TEMPORARY TABLE IF EXISTS mydb.t1")
3773 .await
3774 .unwrap();
3775
3776 assert!(
3778 !sql_context.temp_table_exist("mydb.t1").unwrap(),
3779 "Expected temp table to be gone after DROP"
3780 );
3781 }
3782
3783 #[tokio::test]
3784 async fn test_drop_temp_view_if_exists() {
3785 let catalog = Arc::new(MockCatalog::new());
3786 let sql_context = make_sql_context(catalog).await;
3787
3788 let err = sql_context
3790 .sql("DROP TEMPORARY VIEW mydb.nonexistent")
3791 .await
3792 .unwrap_err();
3793 let msg = err.to_string();
3794 assert!(
3795 msg.contains("doesn't exist")
3796 || msg.contains("does not exist")
3797 || msg.contains("Unknown temp database"),
3798 "Expected view-not-exist error, got: {msg}"
3799 );
3800
3801 sql_context
3803 .sql("DROP TEMPORARY VIEW IF EXISTS mydb.nonexistent")
3804 .await
3805 .unwrap();
3806
3807 sql_context
3809 .sql("CREATE TEMPORARY VIEW mydb.v1 AS SELECT 1 AS id")
3810 .await
3811 .unwrap();
3812
3813 sql_context
3814 .sql("DROP TEMPORARY VIEW IF EXISTS mydb.v1")
3815 .await
3816 .unwrap();
3817
3818 assert!(
3820 !sql_context.temp_table_exist("mydb.v1").unwrap(),
3821 "Expected temp view to be gone after DROP"
3822 );
3823 }
3824
3825 #[test]
3826 fn test_extract_version_as_of() {
3827 let sql = "SELECT id, name FROM paimon.default.time_travel_table VERSION AS OF 1";
3828 let infos = extract_all_version_as_of(sql);
3829 assert_eq!(infos.len(), 1);
3830 let info = &infos[0];
3831 assert_eq!(info.version, "1");
3832 assert_eq!(info.table_name, "paimon.default.time_travel_table");
3833 let rewritten = format!(
3834 "{}__uuid{}",
3835 &sql[..info.clause_range.0],
3836 &sql[info.clause_range.1..]
3837 );
3838 assert_eq!(rewritten, "SELECT id, name FROM __uuid");
3839 }
3840
3841 #[test]
3842 fn test_extract_version_as_of_multi_digit() {
3843 let sql = "SELECT * FROM mydb.t VERSION AS OF 42";
3844 let infos = extract_all_version_as_of(sql);
3845 assert_eq!(infos.len(), 1);
3846 let info = &infos[0];
3847 assert_eq!(info.version, "42");
3848 assert_eq!(info.table_name, "mydb.t");
3849 let rewritten = format!(
3850 "{}__uuid{}",
3851 &sql[..info.clause_range.0],
3852 &sql[info.clause_range.1..]
3853 );
3854 assert_eq!(rewritten, "SELECT * FROM __uuid");
3855 }
3856
3857 #[test]
3858 fn test_extract_version_as_of_case_insensitive() {
3859 let sql = "SELECT * FROM t version as of 5";
3860 let infos = extract_all_version_as_of(sql);
3861 assert_eq!(infos.len(), 1);
3862 let info = &infos[0];
3863 assert_eq!(info.version, "5");
3864 assert_eq!(info.table_name, "t");
3865 let rewritten = format!(
3866 "{}__uuid{}",
3867 &sql[..info.clause_range.0],
3868 &sql[info.clause_range.1..]
3869 );
3870 assert_eq!(rewritten, "SELECT * FROM __uuid");
3871 }
3872
3873 #[test]
3874 fn test_extract_version_as_of_not_present() {
3875 let sql = "SELECT * FROM t";
3876 assert!(extract_all_version_as_of(sql).is_empty());
3877 }
3878
3879 #[test]
3880 fn test_extract_version_as_of_tag() {
3881 let sql = "SELECT id, name FROM paimon.default.t VERSION AS OF 'snapshot1'";
3882 let infos = extract_all_version_as_of(sql);
3883 assert_eq!(infos.len(), 1);
3884 let info = &infos[0];
3885 assert_eq!(info.version, "snapshot1");
3886 assert_eq!(info.table_name, "paimon.default.t");
3887 let rewritten = format!(
3888 "{}__uuid{}",
3889 &sql[..info.clause_range.0],
3890 &sql[info.clause_range.1..]
3891 );
3892 assert_eq!(rewritten, "SELECT id, name FROM __uuid");
3893 }
3894
3895 #[test]
3896 fn test_extract_version_as_of_tag_case_insensitive() {
3897 let sql = "SELECT * FROM t version as of 'my_tag'";
3898 let infos = extract_all_version_as_of(sql);
3899 assert_eq!(infos.len(), 1);
3900 let info = &infos[0];
3901 assert_eq!(info.version, "my_tag");
3902 assert_eq!(info.table_name, "t");
3903 let rewritten = format!(
3904 "{}__uuid{}",
3905 &sql[..info.clause_range.0],
3906 &sql[info.clause_range.1..]
3907 );
3908 assert_eq!(rewritten, "SELECT * FROM __uuid");
3909 }
3910
3911 #[test]
3912 fn test_extract_version_as_of_numeric_still_works() {
3913 let sql = "SELECT * FROM t VERSION AS OF 123";
3914 let infos = extract_all_version_as_of(sql);
3915 assert_eq!(infos.len(), 1);
3916 assert_eq!(infos[0].version, "123");
3917 assert_eq!(infos[0].table_name, "t");
3918 }
3919
3920 #[test]
3921 fn test_extract_version_as_of_multiple() {
3922 let sql = "SELECT * FROM t1 VERSION AS OF 1 JOIN t2 VERSION AS OF 2 ON t1.id = t2.id";
3924 let infos = extract_all_version_as_of(sql);
3925 assert_eq!(infos.len(), 2);
3926 assert_eq!(infos[0].version, "1");
3927 assert_eq!(infos[0].table_name, "t1");
3928 assert_eq!(infos[1].version, "2");
3929 assert_eq!(infos[1].table_name, "t2");
3930 }
3931
3932 #[test]
3933 fn test_extract_version_as_of_skips_string_literal() {
3934 let sql = "SELECT * FROM t WHERE note = 'version as of 1'";
3935 let infos = extract_all_version_as_of(sql);
3936 assert!(infos.is_empty());
3937 }
3938
3939 #[test]
3940 fn test_extract_version_as_of_skips_comment() {
3941 let sql = "SELECT * FROM t -- version as of 1\n WHERE id > 0";
3942 let infos = extract_all_version_as_of(sql);
3943 assert!(infos.is_empty());
3944 }
3945
3946 #[test]
3947 fn test_contains_time_travel_keyword() {
3948 assert!(contains_time_travel_keyword(
3949 "SELECT * FROM t VERSION AS OF 1"
3950 ));
3951 assert!(contains_time_travel_keyword(
3952 "SELECT * FROM t TIMESTAMP AS OF '2024-01-01 00:00:00'"
3953 ));
3954 assert!(!contains_time_travel_keyword(
3956 "SELECT * FROM t WHERE note = 'version as of 1'"
3957 ));
3958 assert!(!contains_time_travel_keyword(
3960 "SELECT * FROM t -- version as of 1"
3961 ));
3962 assert!(!contains_time_travel_keyword(
3963 "SELECT * FROM t /* timestamp as of now */ WHERE id > 0"
3964 ));
3965 assert!(!contains_time_travel_keyword("SELECT * FROM t"));
3967 }
3968
3969 #[test]
3970 fn test_extract_timestamp_as_of() {
3971 let sql = "SELECT * FROM paimon.default.t TIMESTAMP AS OF '2024-01-15 10:30:00'";
3972 let infos = extract_all_timestamp_as_of(sql);
3973 assert_eq!(infos.len(), 1);
3974 let info = &infos[0];
3975 assert_eq!(info.timestamp, "2024-01-15 10:30:00");
3976 assert_eq!(info.table_name, "paimon.default.t");
3977 let rewritten = format!(
3978 "{}__uuid{}",
3979 &sql[..info.clause_range.0],
3980 &sql[info.clause_range.1..]
3981 );
3982 assert_eq!(rewritten, "SELECT * FROM __uuid");
3983 }
3984
3985 #[test]
3986 fn test_extract_timestamp_as_of_case_insensitive() {
3987 let sql = "SELECT * FROM t timestamp as of '2024-06-01 00:00:00'";
3988 let infos = extract_all_timestamp_as_of(sql);
3989 assert_eq!(infos.len(), 1);
3990 let info = &infos[0];
3991 assert_eq!(info.timestamp, "2024-06-01 00:00:00");
3992 assert_eq!(info.table_name, "t");
3993 let rewritten = format!(
3994 "{}__uuid{}",
3995 &sql[..info.clause_range.0],
3996 &sql[info.clause_range.1..]
3997 );
3998 assert_eq!(rewritten, "SELECT * FROM __uuid");
3999 }
4000
4001 #[test]
4002 fn test_extract_timestamp_as_of_not_present() {
4003 let sql = "SELECT * FROM t";
4004 assert!(extract_all_timestamp_as_of(sql).is_empty());
4005 }
4006}