This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 5c80b52 feat(datafusion): implement DROP VIEW functionality in REST
Catalog (#501)
5c80b52 is described below
commit 5c80b52dfe2edc1d2f7d389929ca6ec08cb1a419
Author: QuakeWang <[email protected]>
AuthorDate: Mon Jul 13 13:10:28 2026 +0800
feat(datafusion): implement DROP VIEW functionality in REST Catalog (#501)
---
crates/integrations/datafusion/README.md | 16 +-
crates/integrations/datafusion/src/sql_context.rs | 226 +++++++++++++++++++++-
crates/paimon/src/api/rest_api.rs | 10 +
crates/paimon/src/catalog/mod.rs | 11 ++
crates/paimon/src/catalog/rest/rest_catalog.rs | 11 ++
crates/paimon/tests/mock_server.rs | 46 ++++-
crates/paimon/tests/rest_api_test.rs | 31 +++
crates/paimon/tests/rest_catalog_test.rs | 72 +++++++
docs/src/sql.md | 35 +++-
9 files changed, 442 insertions(+), 16 deletions(-)
diff --git a/crates/integrations/datafusion/README.md
b/crates/integrations/datafusion/README.md
index c4fa4d2..9451572 100644
--- a/crates/integrations/datafusion/README.md
+++ b/crates/integrations/datafusion/README.md
@@ -26,11 +26,11 @@ This crate contains the integration of [Apache
DataFusion](https://datafusion.ap
## REST Catalog views and SQL functions
-`SQLContext` can read, execute, and create persistent views and SQL scalar
functions in a Paimon
-REST Catalog:
+`SQLContext` can read, execute, create, and drop persistent views and can
create SQL scalar functions in a Paimon REST Catalog:
```sql
CREATE VIEW [IF NOT EXISTS] view_name [(column_name, ...)] AS query;
+DROP VIEW [IF EXISTS] view_name;
CREATE FUNCTION [IF NOT EXISTS] function_name([parameter_name data_type, ...])
RETURNS data_type
@@ -45,6 +45,10 @@ RETURN scalar_expression;
column list overrides names only and must match the query output.
Unqualified relations and REST
SQL functions are planned in the new view's owning catalog and database. `IF
NOT EXISTS` is
handled atomically by the catalog.
+- `DROP VIEW` accepts bare, two-part, and three-part names and sends one
direct REST delete request.
+ `IF EXISTS` ignores only a missing view. Multiple targets and `CASCADE`,
`RESTRICT`, `PURGE`, or
+ other drop modifiers are not supported. Catalogs without persistent view
support may return
+ `Unsupported`.
- A SQL function can be called as `function(args...)` in the current
catalog/database or as
`catalog.database.function(args...)`. Its `definitions.datafusion` value
must be a scalar SQL
expression, it must be deterministic, and it must declare its input
parameters and exactly one
@@ -54,10 +58,10 @@ RETURN scalar_expression;
inferred and validated from the planned expression before sending the REST
create request. Bare,
two-part, and three-part creation targets are supported; calls remain
limited to bare and
three-part names.
-- `CREATE OR REPLACE VIEW`, materialized/secure views, comments/options,
persistent `ALTER VIEW` /
- `DROP VIEW`, `CREATE OR REPLACE/ALTER/TEMPORARY FUNCTION`, and persistent
`ALTER FUNCTION` /
- `DROP FUNCTION` are not supported. Lambda/file,
aggregate/table/multi-return, non-deterministic,
- Stable/Volatile, and non-SQL functions are also not supported.
+- `CREATE OR REPLACE VIEW`, materialized/secure views, comments/options,
persistent `ALTER VIEW`,
+ `CREATE OR REPLACE/ALTER/TEMPORARY FUNCTION`, and persistent `ALTER
FUNCTION` / `DROP FUNCTION`
+ are not supported. Lambda/file, aggregate/table/multi-return,
non-deterministic, Stable/Volatile,
+ and non-SQL functions are also not supported.
Use `SQLContext::sql` for function expansion:
diff --git a/crates/integrations/datafusion/src/sql_context.rs
b/crates/integrations/datafusion/src/sql_context.rs
index ec72013..b1e8729 100644
--- a/crates/integrations/datafusion/src/sql_context.rs
+++ b/crates/integrations/datafusion/src/sql_context.rs
@@ -31,6 +31,7 @@
//! - `ALTER TABLE db.t RENAME TO new_name`
//! - `ALTER TABLE db.t DROP PARTITION (col = val, ...)`
//! - `CREATE VIEW [IF NOT EXISTS] view [(col, ...)] AS query`
+//! - `DROP VIEW [IF EXISTS] view`
//! - `CREATE FUNCTION name(args) RETURNS type [LANGUAGE SQL] RETURN
expression`
//! - `TRUNCATE TABLE db.t`
//! - `TRUNCATE TABLE db.t PARTITION (col = val, ...)`
@@ -466,8 +467,11 @@ impl SQLContext {
object_type,
if_exists,
names,
+ cascade,
+ restrict,
+ purge,
temporary,
- ..
+ table,
} if matches!(*object_type, ObjectType::Table | ObjectType::View)
=> {
if *temporary {
self.handle_drop_temp_table(names, *if_exists)
@@ -482,7 +486,42 @@ impl SQLContext {
self.ctx.sql(sql).await
}
} else {
- self.ctx.sql(sql).await
+ let targets_paimon_catalog = names.iter().any(|name| {
+ let table_ref: TableReference =
name.to_string().as_str().into();
+ self.is_paimon_catalog_ref(&table_ref)
+ });
+ if !targets_paimon_catalog {
+ return self.ctx.sql(sql).await;
+ }
+ let [name] = names.as_slice() else {
+ return Err(DataFusionError::Plan(
+ "Persistent DROP VIEW does not support multiple
views".to_string(),
+ ));
+ };
+ if *cascade {
+ return Err(DataFusionError::Plan(
+ "DROP VIEW CASCADE is not supported".to_string(),
+ ));
+ }
+ if *restrict {
+ return Err(DataFusionError::Plan(
+ "DROP VIEW RESTRICT is not supported".to_string(),
+ ));
+ }
+ if *purge {
+ return Err(DataFusionError::Plan(
+ "DROP VIEW PURGE is not supported".to_string(),
+ ));
+ }
+ if table.is_some() {
+ return Err(DataFusionError::Plan(
+ "DROP VIEW ON clauses are not
supported".to_string(),
+ ));
+ }
+ let (catalog, _catalog_name, identifier) =
+ self.resolve_catalog_and_table(name)?;
+ self.handle_drop_view(&catalog, &identifier, *if_exists)
+ .await
}
}
Statement::Call(func) => {
@@ -940,6 +979,19 @@ impl SQLContext {
ok_result(&self.ctx)
}
+ async fn handle_drop_view(
+ &self,
+ catalog: &Arc<dyn Catalog>,
+ identifier: &Identifier,
+ if_exists: bool,
+ ) -> DFResult<DataFrame> {
+ catalog
+ .drop_view(identifier, if_exists)
+ .await
+ .map_err(to_datafusion_error)?;
+ ok_result(&self.ctx)
+ }
+
async fn handle_show_create_table(&self, sql: &str, name: &ObjectName) ->
DFResult<DataFrame> {
let (catalog, catalog_name, identifier) =
self.resolve_catalog_and_table(name)?;
let table = match catalog.get_table(&identifier).await {
@@ -3256,6 +3308,7 @@ mod tests {
existing_table: Mutex<Option<Table>>,
functions: Mutex<HashMap<Identifier, paimon::catalog::Function>>,
views: Mutex<HashMap<Identifier, paimon::catalog::View>>,
+ drop_view_supported: bool,
}
impl MockCatalog {
@@ -3265,6 +3318,14 @@ mod tests {
existing_table: Mutex::new(None),
functions: Mutex::new(HashMap::new()),
views: Mutex::new(HashMap::new()),
+ drop_view_supported: true,
+ }
+ }
+
+ fn without_drop_view_support() -> Self {
+ Self {
+ drop_view_supported: false,
+ ..Self::new()
}
}
@@ -3455,6 +3516,25 @@ mod tests {
);
Ok(())
}
+
+ async fn drop_view(
+ &self,
+ identifier: &Identifier,
+ ignore_if_not_exists: bool,
+ ) -> paimon::Result<()> {
+ if !self.drop_view_supported {
+ return Err(paimon::Error::Unsupported {
+ message: "Catalog does not support views".to_string(),
+ });
+ }
+ if self.views.lock().unwrap().remove(identifier).is_some() ||
ignore_if_not_exists {
+ Ok(())
+ } else {
+ Err(paimon::Error::ViewNotExist {
+ full_name: identifier.full_name(),
+ })
+ }
+ }
}
async fn make_sql_context(catalog: Arc<MockCatalog>) -> SQLContext {
@@ -3571,6 +3651,148 @@ mod tests {
assert_eq!(answers.value(0), 42);
}
+ #[tokio::test]
+ async fn persistent_rest_catalog_view_can_be_dropped() {
+ let catalog = Arc::new(MockCatalog::new());
+ let ctx = make_sql_context(Arc::clone(&catalog)).await;
+ ctx.sql(
+ "CREATE VIEW answer_view AS \
+ SELECT CAST(42 AS BIGINT) AS answer",
+ )
+ .await
+ .unwrap();
+
+ ctx.sql("DROP VIEW answer_view").await.unwrap();
+
+ assert!(matches!(
+ catalog
+ .get_view(&Identifier::new("default", "answer_view"))
+ .await,
+ Err(paimon::Error::ViewNotExist { .. })
+ ));
+ assert!(ctx.sql("SELECT * FROM answer_view").await.is_err());
+ }
+
+ #[tokio::test]
+ async fn persistent_rest_catalog_view_drop_honors_if_exists() {
+ let catalog = Arc::new(MockCatalog::new());
+ let ctx = make_sql_context(catalog).await;
+
+ let error = ctx.sql("DROP VIEW missing_view").await.unwrap_err();
+ assert!(
+ error
+ .to_string()
+ .contains("View default.missing_view does not exist"),
+ "unexpected error: {error}"
+ );
+ ctx.sql("DROP VIEW IF EXISTS missing_view").await.unwrap();
+ }
+
+ #[tokio::test]
+ async fn persistent_rest_catalog_view_drop_resolves_supported_name_forms()
{
+ let catalog = Arc::new(MockCatalog::new());
+ add_bigint_view(&catalog, "default", "bare_view", "SELECT 1 AS
answer");
+ add_bigint_view(&catalog, "other", "two_part", "SELECT 1 AS answer");
+ add_bigint_view(&catalog, "other", "three_part", "SELECT 1 AS answer");
+ add_bigint_view(&catalog, "default", "Quoted View", "SELECT 1 AS
answer");
+ let ctx = make_sql_context(Arc::clone(&catalog)).await;
+
+ ctx.sql("DROP VIEW bare_view").await.unwrap();
+ ctx.sql("DROP VIEW IF EXISTS other.two_part").await.unwrap();
+ ctx.sql("DROP VIEW IF EXISTS paimon.other.three_part")
+ .await
+ .unwrap();
+ ctx.sql("DROP VIEW \"Quoted View\"").await.unwrap();
+
+ for identifier in [
+ Identifier::new("default", "bare_view"),
+ Identifier::new("other", "two_part"),
+ Identifier::new("other", "three_part"),
+ Identifier::new("default", "Quoted View"),
+ ] {
+ assert!(matches!(
+ catalog.get_view(&identifier).await,
+ Err(paimon::Error::ViewNotExist { .. })
+ ));
+ }
+ }
+
+ #[tokio::test]
+ async fn persistent_rest_catalog_view_drop_rejects_unsupported_modifiers()
{
+ let cases = [
+ ("DROP VIEW paimon.default.invalid_view CASCADE", "CASCADE"),
+ ("DROP VIEW paimon.default.invalid_view RESTRICT", "RESTRICT"),
+ ("DROP VIEW paimon.default.invalid_view PURGE", "PURGE"),
+ (
+ "DROP VIEW paimon.default.invalid_view ON default.target",
+ "ON clauses",
+ ),
+ ];
+
+ for (sql, modifier) in cases {
+ let catalog = Arc::new(MockCatalog::new());
+ let ctx = make_sql_context(catalog).await;
+ let error = ctx.sql(sql).await.unwrap_err();
+ assert!(
+ error.to_string().contains(modifier),
+ "expected {modifier} error, got: {error}"
+ );
+ }
+ }
+
+ #[tokio::test]
+ async fn
persistent_rest_catalog_view_drop_rejects_multiple_targets_before_deleting() {
+ let catalog = Arc::new(MockCatalog::new());
+ add_bigint_view(&catalog, "default", "first", "SELECT 1 AS answer");
+ let ctx = make_sql_context(Arc::clone(&catalog)).await;
+
+ let error = ctx
+ .sql("DROP VIEW paimon.default.first, datafusion.public.second")
+ .await
+ .unwrap_err();
+
+ assert!(error
+ .to_string()
+ .contains("Persistent DROP VIEW does not support multiple views"));
+ assert!(catalog
+ .get_view(&Identifier::new("default", "first"))
+ .await
+ .is_ok());
+ }
+
+ #[tokio::test]
+ async fn
persistent_rest_catalog_view_drop_propagates_unsupported_catalog() {
+ let catalog = Arc::new(MockCatalog::without_drop_view_support());
+ add_bigint_view(&catalog, "default", "answer_view", "SELECT 1 AS
answer");
+ let ctx = make_sql_context(Arc::clone(&catalog)).await;
+
+ let error = ctx.sql("DROP VIEW answer_view").await.unwrap_err();
+
+ assert!(error.to_string().contains("Catalog does not support views"));
+ assert!(catalog
+ .get_view(&Identifier::new("default", "answer_view"))
+ .await
+ .is_ok());
+ }
+
+ #[tokio::test]
+ async fn persistent_rest_catalog_view_drop_delegates_non_paimon_catalog() {
+ let catalog = Arc::new(MockCatalog::new());
+ let ctx = make_sql_context(catalog).await;
+ ctx.sql("CREATE VIEW datafusion.public.delegated_view AS SELECT 1 AS
answer")
+ .await
+ .unwrap();
+
+ ctx.sql("DROP VIEW datafusion.public.delegated_view")
+ .await
+ .unwrap();
+
+ assert!(ctx
+ .sql("SELECT * FROM datafusion.public.delegated_view")
+ .await
+ .is_err());
+ }
+
#[tokio::test]
async fn persistent_rest_catalog_function_can_be_created_and_called() {
let catalog = Arc::new(MockCatalog::new());
diff --git a/crates/paimon/src/api/rest_api.rs
b/crates/paimon/src/api/rest_api.rs
index 5d60b33..5110133 100644
--- a/crates/paimon/src/api/rest_api.rs
+++ b/crates/paimon/src/api/rest_api.rs
@@ -467,6 +467,16 @@ impl RESTApi {
self.client.get(&path, None::<&[(&str, &str)]>).await
}
+ /// Drop a persistent view.
+ pub async fn drop_view(&self, identifier: &Identifier) -> Result<()> {
+ let database = identifier.database();
+ let view = identifier.object();
+ validate_non_empty_multi(&[(database, "database name"), (view, "view
name")])?;
+ let path = self.resource_paths.view(database, view);
+ let _resp: serde_json::Value = self.client.delete(&path,
None::<&[(&str, &str)]>).await?;
+ Ok(())
+ }
+
// ==================== Function Operations ====================
/// Create a persistent function.
diff --git a/crates/paimon/src/catalog/mod.rs b/crates/paimon/src/catalog/mod.rs
index 516b830..38a414b 100644
--- a/crates/paimon/src/catalog/mod.rs
+++ b/crates/paimon/src/catalog/mod.rs
@@ -389,6 +389,17 @@ pub trait Catalog: Send + Sync {
})
}
+ /// Drop a persistent view.
+ ///
+ /// # Errors
+ /// * [`crate::Error::ViewNotExist`] - view does not exist when
+ /// `ignore_if_not_exists` is false.
+ async fn drop_view(&self, _identifier: &Identifier, _ignore_if_not_exists:
bool) -> Result<()> {
+ Err(Error::Unsupported {
+ message: "Catalog does not support views".to_string(),
+ })
+ }
+
/// List persistent view names in a database.
async fn list_views(&self, _database_name: &str) -> Result<Vec<String>> {
Err(Error::Unsupported {
diff --git a/crates/paimon/src/catalog/rest/rest_catalog.rs
b/crates/paimon/src/catalog/rest/rest_catalog.rs
index 9fbb388..2057d3f 100644
--- a/crates/paimon/src/catalog/rest/rest_catalog.rs
+++ b/crates/paimon/src/catalog/rest/rest_catalog.rs
@@ -313,6 +313,17 @@ impl Catalog for RESTCatalog {
))
}
+ async fn drop_view(&self, identifier: &Identifier, ignore_if_not_exists:
bool) -> Result<()> {
+ let result = self
+ .api
+ .drop_view(identifier)
+ .await
+ .map_err(|error| map_rest_error_for_view(error, identifier));
+ ignore_error_if(result, |error| {
+ ignore_if_not_exists && matches!(error, Error::ViewNotExist { .. })
+ })
+ }
+
async fn list_functions(&self, database_name: &str) -> Result<Vec<String>>
{
self.api
.list_functions(database_name)
diff --git a/crates/paimon/tests/mock_server.rs
b/crates/paimon/tests/mock_server.rs
index 21e9520..6c8a004 100644
--- a/crates/paimon/tests/mock_server.rs
+++ b/crates/paimon/tests/mock_server.rs
@@ -49,6 +49,7 @@ struct MockState {
views: HashMap<String, GetViewResponse>,
functions: HashMap<String, GetFunctionResponse>,
view_function_endpoints_unsupported: bool,
+ drop_view_error_status: Option<StatusCode>,
list_page_size: Option<usize>,
no_permission_databases: HashSet<String>,
no_permission_tables: HashSet<String>,
@@ -369,6 +370,44 @@ impl RESTServer {
}
}
+ /// Handle DELETE /databases/:db/views/:view - drop a persistent view.
+ pub async fn drop_view(
+ Path((db, view)): Path<(String, String)>,
+ Extension(state): Extension<Arc<RESTServer>>,
+ ) -> impl IntoResponse {
+ let mut s = state.inner.lock().unwrap();
+ if s.view_function_endpoints_unsupported {
+ let err = ErrorResponse::new(
+ Some("view".to_string()),
+ Some(view),
+ Some("Not Implemented".to_string()),
+ Some(501),
+ );
+ return (StatusCode::NOT_IMPLEMENTED, Json(err)).into_response();
+ }
+ if let Some(status) = s.drop_view_error_status {
+ let err = ErrorResponse::new(
+ Some("view".to_string()),
+ Some(view),
+ status.canonical_reason().map(ToString::to_string),
+ Some(status.as_u16() as i32),
+ );
+ return (status, Json(err)).into_response();
+ }
+ let key = format!("{db}.{view}");
+ if s.views.remove(&key).is_some() {
+ (StatusCode::OK, Json(serde_json::json!(""))).into_response()
+ } else {
+ let err = ErrorResponse::new(
+ Some("view".to_string()),
+ Some(view),
+ Some("Not Found".to_string()),
+ Some(404),
+ );
+ (StatusCode::NOT_FOUND, Json(err)).into_response()
+ }
+ }
+
/// Handle GET /databases/:db/views - list persistent views.
pub async fn list_views(
Path(db): Path<String>,
@@ -886,6 +925,11 @@ impl RESTServer {
.view_function_endpoints_unsupported = true;
}
+ /// Make the drop-view endpoint return the given status.
+ pub fn set_drop_view_error_status(&self, status: Option<StatusCode>) {
+ self.inner.lock().unwrap().drop_view_error_status = status;
+ }
+
/// Add a table with schema and path to the server state.
///
/// This is needed for `RESTCatalog::get_table` which requires
@@ -1051,7 +1095,7 @@ pub async fn start_mock_server(
)
.route(
&format!("{prefix}/databases/:db/views/:view"),
- get(RESTServer::get_view),
+ get(RESTServer::get_view).delete(RESTServer::drop_view),
)
.route(
&format!("{prefix}/databases/:db/functions"),
diff --git a/crates/paimon/tests/rest_api_test.rs
b/crates/paimon/tests/rest_api_test.rs
index e020b6c..99b952c 100644
--- a/crates/paimon/tests/rest_api_test.rs
+++ b/crates/paimon/tests/rest_api_test.rs
@@ -134,6 +134,37 @@ async fn test_create_view() {
assert_eq!(response.schema.query(), "SELECT id FROM source");
}
+#[tokio::test]
+async fn test_drop_view_uses_encoded_individual_view_path() {
+ let database = "db+%";
+ let view = "active+?#%";
+ let ctx = setup_test_server(vec![database]).await;
+ let schema = ViewSchema::new(
+ Vec::new(),
+ "SELECT 1".to_string(),
+ HashMap::new(),
+ None,
+ HashMap::new(),
+ );
+ let identifier = Identifier::new(database, view);
+ ctx.api.create_view(&identifier, schema).await.unwrap();
+
+ ctx.api.drop_view(&identifier).await.unwrap();
+
+ assert!(matches!(
+ ctx.api.get_view(&identifier).await.unwrap_err(),
+ paimon::Error::RestApi {
+ source: paimon::api::RestError::NoSuchResource { .. }
+ }
+ ));
+ assert!(matches!(
+ ctx.api.drop_view(&identifier).await.unwrap_err(),
+ paimon::Error::RestApi {
+ source: paimon::api::RestError::NoSuchResource { .. }
+ }
+ ));
+}
+
#[tokio::test]
async fn test_list_views() {
let ctx = setup_test_server(vec!["default"]).await;
diff --git a/crates/paimon/tests/rest_catalog_test.rs
b/crates/paimon/tests/rest_catalog_test.rs
index 849b699..2000f9e 100644
--- a/crates/paimon/tests/rest_catalog_test.rs
+++ b/crates/paimon/tests/rest_catalog_test.rs
@@ -25,6 +25,7 @@ use std::sync::Arc;
use arrow_array::{Array, BinaryArray, Int32Array, Int64Array, RecordBatch,
StringArray};
use arrow_schema::{DataType as ArrowDataType, Field as ArrowField, Schema as
ArrowSchema};
+use axum::http::StatusCode;
use futures::TryStreamExt;
use paimon::api::ConfigResponse;
use paimon::catalog::{Catalog, Function, FunctionDefinition, Identifier,
RESTCatalog, ViewSchema};
@@ -1052,6 +1053,70 @@ async fn test_catalog_create_view() {
assert_eq!(view.query_for("datafusion"), "SELECT id FROM source");
}
+#[tokio::test]
+async fn test_catalog_drop_view() {
+ let ctx = setup_catalog(vec!["default"]).await;
+ let identifier = Identifier::new("default", "active_ids");
+ ctx.catalog
+ .create_view(
+ &identifier,
+ ViewSchema::new(
+ Vec::new(),
+ "SELECT 1".to_string(),
+ HashMap::new(),
+ None,
+ HashMap::new(),
+ ),
+ false,
+ )
+ .await
+ .unwrap();
+
+ ctx.catalog.drop_view(&identifier, false).await.unwrap();
+
+ assert!(ctx.catalog.list_views("default").await.unwrap().is_empty());
+ assert!(matches!(
+ ctx.catalog.get_view(&identifier).await.unwrap_err(),
+ paimon::Error::ViewNotExist { full_name } if full_name ==
"default.active_ids"
+ ));
+}
+
+#[tokio::test]
+async fn test_catalog_drop_missing_view_honors_ignore_if_not_exists() {
+ let ctx = setup_catalog(vec!["default"]).await;
+ let identifier = Identifier::new("default", "missing");
+
+ assert!(matches!(
+ ctx.catalog.drop_view(&identifier, false).await.unwrap_err(),
+ paimon::Error::ViewNotExist { full_name } if full_name ==
"default.missing"
+ ));
+ ctx.catalog.drop_view(&identifier, true).await.unwrap();
+}
+
+#[tokio::test]
+async fn test_catalog_drop_view_if_exists_does_not_hide_other_errors() {
+ let ctx = setup_catalog(vec!["default"]).await;
+ let identifier = Identifier::new("default", "active_ids");
+
+ ctx.server
+ .set_drop_view_error_status(Some(StatusCode::FORBIDDEN));
+ assert!(matches!(
+ ctx.catalog.drop_view(&identifier, true).await.unwrap_err(),
+ paimon::Error::RestApi {
+ source: paimon::api::RestError::Forbidden { .. }
+ }
+ ));
+
+ ctx.server
+ .set_drop_view_error_status(Some(StatusCode::INTERNAL_SERVER_ERROR));
+ assert!(matches!(
+ ctx.catalog.drop_view(&identifier, true).await.unwrap_err(),
+ paimon::Error::RestApi {
+ source: paimon::api::RestError::ServiceFailure { .. }
+ }
+ ));
+}
+
#[tokio::test]
async fn test_catalog_create_view_missing_database() {
let ctx = setup_catalog(vec!["default"]).await;
@@ -1399,6 +1464,13 @@ async fn
test_catalog_maps_unsupported_view_and_function_endpoints() {
.unwrap_err(),
paimon::Error::Unsupported { .. }
));
+ assert!(matches!(
+ ctx.catalog
+ .drop_view(&Identifier::new("default", "view"), true)
+ .await
+ .unwrap_err(),
+ paimon::Error::Unsupported { .. }
+ ));
assert!(matches!(
ctx.catalog.list_functions("default").await.unwrap_err(),
paimon::Error::Unsupported { .. }
diff --git a/docs/src/sql.md b/docs/src/sql.md
index 50623e4..587603e 100644
--- a/docs/src/sql.md
+++ b/docs/src/sql.md
@@ -40,7 +40,7 @@ Mosaic support is always available and currently read-only.
SQL queries can read
SQL support has two layers:
- DataFusion provides the parser, query planner, optimizer, execution engine,
expressions, scalar functions, aggregate functions, and window functions. SQL
statements that `SQLContext` does not intercept are delegated to DataFusion.
This includes the DataFusion SQL surface for `SELECT` queries, CTEs (including
recursive CTEs), subqueries, joins including `LATERAL` joins, SQL lambda
functions, grouping, `HAVING`, window clauses, `QUALIFY`, set operations,
`ORDER BY`, `LIMIT`/`OFFSET`, `EX [...]
-- Paimon-specific table management and row-level writes are implemented by
`SQLContext`. This includes Paimon `CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`,
`CREATE TEMPORARY TABLE`, `CREATE TEMPORARY VIEW`, REST Catalog persistent
`CREATE VIEW` and `CREATE FUNCTION`, `DROP TEMPORARY TABLE` / `VIEW`, `INSERT
OVERWRITE ... PARTITION`, `UPDATE`, `DELETE`, `MERGE INTO`, `TRUNCATE TABLE`,
`ALTER TABLE ... DROP PARTITION`, `CALL sys.*`, Paimon time travel, and `SET` /
`RESET 'paimon.*'`.
+- Paimon-specific table management and row-level writes are implemented by
`SQLContext`. This includes Paimon `CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`,
`CREATE TEMPORARY TABLE`, `CREATE TEMPORARY VIEW`, REST Catalog persistent
`CREATE VIEW`, `DROP VIEW`, and `CREATE FUNCTION`, `DROP TEMPORARY TABLE` /
`VIEW`, `INSERT OVERWRITE ... PARTITION`, `UPDATE`, `DELETE`, `MERGE INTO`,
`TRUNCATE TABLE`, `ALTER TABLE ... DROP PARTITION`, `CALL sys.*`, Paimon time
travel, and `SET` / `RESET 'paimon.*'`.
Not every DataFusion DDL/DML statement maps to a Paimon table operation. For
Paimon catalogs, `CREATE EXTERNAL TABLE`, `LOCATION`, `CREATE MATERIALIZED
VIEW`, and persistent `CREATE TABLE AS SELECT` are rejected or not implemented.
Persistent `CREATE FUNCTION` is supported only for the REST Catalog SQL scalar
form documented below. DataFusion `COPY` can export query results to files; it
does not create or commit Paimon table files.
@@ -78,8 +78,7 @@ internally for `SET`/`RESET` support.
### REST Catalog Views and SQL Functions
-When the registered catalog is a Paimon REST Catalog, `SQLContext` can read,
-execute, and create persistent views and SQL scalar functions.
+When the registered catalog is a Paimon REST Catalog, `SQLContext` can read,
execute, create, and drop persistent views and can create SQL scalar functions.
Create a persistent view with this syntax:
@@ -87,6 +86,12 @@ Create a persistent view with this syntax:
CREATE VIEW [IF NOT EXISTS] view_name [(column_name, ...)] AS query;
```
+Drop a persistent view with this syntax:
+
+```sql
+DROP VIEW [IF EXISTS] view_name;
+```
+
For example:
```sql
@@ -107,10 +112,14 @@ the new view's owning catalog and database, not the
session's current database.
The canonical query is stored as both the default query and the `datafusion`
dialect definition.
-Persistent `CREATE VIEW` is currently implemented by REST Catalog. Other
-catalog implementations may return `Unsupported`. `CREATE OR REPLACE VIEW`,
-materialized/secure views, view comments or options, vendor-specific modifiers,
-and persistent `ALTER VIEW` / `DROP VIEW` are not supported.
+Persistent `CREATE VIEW` and `DROP VIEW` are currently implemented by REST
+Catalog. `DROP VIEW` sends a direct delete request; `IF EXISTS` ignores only a
+missing-view response. Bare, two-part, and three-part names are supported, but
+only one target may be dropped per statement. Other catalog implementations may
+return `Unsupported`. `CREATE OR REPLACE VIEW`, materialized/secure views, view
+comments or options, vendor-specific create modifiers, persistent `ALTER VIEW`,
+and `DROP VIEW` modifiers such as `CASCADE`, `RESTRICT`, or `PURGE` are not
+supported.
Persistent views resolve through the normal DataFusion catalog path, so they
can be queried wherever a table can be used:
@@ -588,6 +597,18 @@ DROP TABLE paimon.my_db.users;
DROP TABLE IF EXISTS paimon.my_db.users;
```
+### DROP VIEW
+
+Drop one persistent view from a REST Catalog:
+
+```sql
+DROP VIEW active_users;
+DROP VIEW IF EXISTS my_db.active_users;
+DROP VIEW IF EXISTS paimon.my_db.active_users;
+```
+
+`IF EXISTS` ignores only a missing view; authorization, server, and network
errors are still returned. Only one persistent view target is supported per
statement. `CASCADE`, `RESTRICT`, `PURGE`, and other drop modifiers are
rejected. Catalog implementations without persistent view support return
`Unsupported`.
+
### CREATE TEMPORARY TABLE
Create an in-memory temporary table from a query result. Temporary tables
exist only for the lifetime of the `SQLContext` instance and are automatically
cleaned up when the context is dropped.