mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-11-15 07:17:01 +00:00
* refactor(event): separate procedure submission context Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(event): map extensions and forward GC context Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(gc): initialize integration test context Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(test): pass procedure context to DDL helpers Signed-off-by: WenyXu <wenymedia@gmail.com> * refactor: simplify procedure submission contexts Signed-off-by: WenyXu <wenymedia@gmail.com> * refactor(event): separate procedure and query contexts Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(event): clarify procedure context propagation Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(event): preserve procedure submission context Signed-off-by: WenyXu <wenymedia@gmail.com> * refactor(event): move DDL context by value Signed-off-by: WenyXu <wenymedia@gmail.com> * fix(test): retain manual GC event context Signed-off-by: WenyXu <wenymedia@gmail.com> * refactor(event): tighten procedure context API Signed-off-by: WenyXu <wenymedia@gmail.com> * chore: update greptime-proto Signed-off-by: WenyXu <wenymedia@gmail.com> --------- Signed-off-by: WenyXu <wenymedia@gmail.com>
366 lines
13 KiB
Rust
366 lines
13 KiB
Rust
// Copyright 2023-2026 GrepTime Inc.
|
|
//
|
|
// This file is part of the GreptimeDB Enterprise Edition and is licensed under
|
|
// the GreptimeDB Enterprise License. You may not use this file except in
|
|
// compliance with that license. A copy of the license is available at the root
|
|
// of this repository in the file LICENSE-ENTERPRISE.
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, this software is
|
|
// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
|
// either express or implied.
|
|
|
|
use arrow::datatypes::DataType as ArrowDataType;
|
|
use common_error::ext::BoxedError;
|
|
use common_macro::admin_fn;
|
|
use common_query::error::{
|
|
InvalidFuncArgsSnafu, MissingProcedureServiceHandlerSnafu, ProcedureServiceSnafu, Result,
|
|
UnsupportedInputDataTypeSnafu,
|
|
};
|
|
use datafusion_expr::{Signature, Volatility};
|
|
use datatypes::prelude::{Value, ValueRef};
|
|
use session::context::QueryContextRef;
|
|
use session::table_name::table_name_to_full_name;
|
|
use snafu::{ResultExt, ensure};
|
|
use table::table_name::TableName;
|
|
|
|
use crate::handlers::ProcedureServiceHandlerRef;
|
|
|
|
/// Purges a dropped table after the ADMIN statement layer authorizes the request.
|
|
/// Name validation retains the session helper's existing catalog-only permission check.
|
|
#[admin_fn(
|
|
name = PurgeTableFunction,
|
|
display_name = purge_table,
|
|
sig_fn = purge_table_signature,
|
|
ret = uint64,
|
|
single_row
|
|
)]
|
|
pub(crate) async fn purge_table(
|
|
procedure_service_handler: &ProcedureServiceHandlerRef,
|
|
query_ctx: &QueryContextRef,
|
|
params: &[ValueRef<'_>],
|
|
) -> Result<Value> {
|
|
ensure!(
|
|
params.len() == 1,
|
|
InvalidFuncArgsSnafu {
|
|
err_msg: format!(
|
|
"The length of the args is not correct, expect 1, have: {}",
|
|
params.len()
|
|
),
|
|
}
|
|
);
|
|
let ValueRef::String(table_name) = params[0] else {
|
|
return UnsupportedInputDataTypeSnafu {
|
|
function: "purge_table",
|
|
datatypes: params
|
|
.iter()
|
|
.map(|value| value.data_type())
|
|
.collect::<Vec<_>>(),
|
|
}
|
|
.fail();
|
|
};
|
|
let (catalog_name, schema_name, table_name) = table_name_to_full_name(table_name, query_ctx)
|
|
.map_err(BoxedError::new)
|
|
.context(ProcedureServiceSnafu)?;
|
|
|
|
procedure_service_handler
|
|
.purge_table(
|
|
query_ctx.clone(),
|
|
TableName::new(catalog_name, schema_name, table_name),
|
|
)
|
|
.await?;
|
|
Ok(Value::from(0_u64))
|
|
}
|
|
|
|
fn purge_table_signature() -> Signature {
|
|
Signature::uniform(1, vec![ArrowDataType::Utf8], Volatility::Immutable)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use api::v1::meta::ReconcileRequest;
|
|
use arrow::array::StringArray;
|
|
use arrow::datatypes::{DataType as ArrowDataType, Field};
|
|
use async_trait::async_trait;
|
|
use catalog::CatalogManagerRef;
|
|
use common_macro::admin_fn;
|
|
use common_meta::rpc::procedure::{
|
|
GcRegionsRequest, GcResponse, GcTableRequest, ManageRegionFollowerRequest,
|
|
MigrateRegionRequest, ProcedureStateResponse,
|
|
};
|
|
use common_query::error::{InvalidFuncArgsSnafu, MissingProcedureServiceHandlerSnafu, Result};
|
|
use datafusion_expr::{ColumnarValue, Signature, TypeSignature, Volatility};
|
|
use datatypes::prelude::{Value, ValueRef};
|
|
use session::context::{QueryContext, QueryContextBuilder, QueryContextRef};
|
|
use table::table_name::TableName;
|
|
|
|
use super::{PurgeTableFunction, purge_table};
|
|
use crate::function_factory::ScalarFunctionFactory;
|
|
use crate::function_registry::{FUNCTION_REGISTRY, get_admin_function};
|
|
use crate::handlers::{ProcedureServiceHandler, ProcedureServiceHandlerRef};
|
|
use crate::state::FunctionState;
|
|
|
|
#[admin_fn(
|
|
name = ZeroArgSingleRowFunction,
|
|
display_name = zero_arg_single_row,
|
|
sig_fn = zero_arg_single_row_signature,
|
|
ret = uint64,
|
|
single_row
|
|
)]
|
|
async fn zero_arg_single_row(
|
|
_procedure_service_handler: &ProcedureServiceHandlerRef,
|
|
_query_ctx: &QueryContextRef,
|
|
params: &[ValueRef<'_>],
|
|
) -> Result<Value> {
|
|
assert!(params.is_empty());
|
|
Ok(Value::from(0_u64))
|
|
}
|
|
|
|
fn zero_arg_single_row_signature() -> Signature {
|
|
Signature::nullary(Volatility::Immutable)
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct RecordingHandler {
|
|
calls: Mutex<Vec<(TableName, QueryContextRef)>>,
|
|
fail: bool,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ProcedureServiceHandler for RecordingHandler {
|
|
async fn purge_table(
|
|
&self,
|
|
query_ctx: QueryContextRef,
|
|
table_name: TableName,
|
|
) -> Result<()> {
|
|
if self.fail {
|
|
return InvalidFuncArgsSnafu {
|
|
err_msg: "purge failed",
|
|
}
|
|
.fail();
|
|
}
|
|
self.calls.lock().unwrap().push((table_name, query_ctx));
|
|
Ok(())
|
|
}
|
|
|
|
async fn migrate_region(
|
|
&self,
|
|
_: QueryContextRef,
|
|
_: MigrateRegionRequest,
|
|
) -> Result<Option<String>> {
|
|
unreachable!()
|
|
}
|
|
async fn reconcile(&self, _: ReconcileRequest) -> Result<Option<String>> {
|
|
unreachable!()
|
|
}
|
|
async fn query_procedure_state(&self, _: &str) -> Result<ProcedureStateResponse> {
|
|
unreachable!()
|
|
}
|
|
async fn manage_region_follower(&self, _: ManageRegionFollowerRequest) -> Result<()> {
|
|
unreachable!()
|
|
}
|
|
fn catalog_manager(&self) -> &CatalogManagerRef {
|
|
unreachable!()
|
|
}
|
|
async fn gc_regions(&self, _: QueryContextRef, _: GcRegionsRequest) -> Result<GcResponse> {
|
|
unreachable!()
|
|
}
|
|
async fn gc_table(&self, _: QueryContextRef, _: GcTableRequest) -> Result<GcResponse> {
|
|
unreachable!()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_purge_table_is_admin_only() {
|
|
assert!(get_admin_function("purge_table").is_some());
|
|
assert!(FUNCTION_REGISTRY.get_function("purge_table").is_none());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_admin_only_factory_executes_without_registry_leak() {
|
|
let handler = Arc::new(RecordingHandler::default());
|
|
let state = FunctionState {
|
|
procedure_service_handler: Some(handler.clone()),
|
|
..Default::default()
|
|
};
|
|
let function =
|
|
get_admin_function("purge_table")
|
|
.unwrap()
|
|
.provide(crate::function::FunctionContext {
|
|
query_ctx: QueryContext::arc(),
|
|
state: Arc::new(state),
|
|
});
|
|
let args = datafusion_expr::ScalarFunctionArgs {
|
|
args: vec![ColumnarValue::Array(Arc::new(StringArray::from(vec![
|
|
"foo",
|
|
])))],
|
|
arg_fields: vec![Arc::new(Field::new("arg_0", ArrowDataType::Utf8, false))],
|
|
return_field: Arc::new(Field::new("result", ArrowDataType::UInt64, false)),
|
|
number_rows: 1,
|
|
config_options: Arc::new(datafusion_common::config::ConfigOptions::default()),
|
|
};
|
|
|
|
function
|
|
.as_async()
|
|
.unwrap()
|
|
.invoke_async_with_args(args)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(1, handler.calls.lock().unwrap().len());
|
|
assert!(FUNCTION_REGISTRY.get_function("purge_table").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_purge_table_signature() {
|
|
let factory: ScalarFunctionFactory = PurgeTableFunction::factory().into();
|
|
let function = factory.provide(crate::function::FunctionContext::mock());
|
|
assert_eq!("purge_table", function.name());
|
|
assert_eq!(ArrowDataType::UInt64, function.return_type(&[]).unwrap());
|
|
assert!(matches!(
|
|
function.signature(),
|
|
Signature { type_signature: TypeSignature::Uniform(1, types), volatility: Volatility::Immutable, .. }
|
|
if types == &vec![ArrowDataType::Utf8]
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_purge_table_rejects_multi_row_before_handler_call() {
|
|
let handler = Arc::new(RecordingHandler::default());
|
|
let state = FunctionState {
|
|
procedure_service_handler: Some(handler.clone()),
|
|
..Default::default()
|
|
};
|
|
let factory: ScalarFunctionFactory = PurgeTableFunction::factory().into();
|
|
let function = factory.provide(crate::function::FunctionContext {
|
|
query_ctx: QueryContext::arc(),
|
|
state: Arc::new(state),
|
|
});
|
|
let args = datafusion_expr::ScalarFunctionArgs {
|
|
args: vec![ColumnarValue::Array(Arc::new(StringArray::from(vec![
|
|
Some("foo"),
|
|
None,
|
|
])))],
|
|
arg_fields: vec![Arc::new(Field::new("arg_0", ArrowDataType::Utf8, true))],
|
|
return_field: Arc::new(Field::new("result", ArrowDataType::UInt64, true)),
|
|
number_rows: 2,
|
|
config_options: Arc::new(datafusion_common::config::ConfigOptions::default()),
|
|
};
|
|
|
|
assert!(
|
|
function
|
|
.as_async()
|
|
.unwrap()
|
|
.invoke_async_with_args(args)
|
|
.await
|
|
.is_err()
|
|
);
|
|
assert!(handler.calls.lock().unwrap().is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_single_row_admin_fn_rejects_zero_arg_multi_row_invocation() {
|
|
let handler = Arc::new(RecordingHandler::default());
|
|
let state = FunctionState {
|
|
procedure_service_handler: Some(handler),
|
|
..Default::default()
|
|
};
|
|
let factory: ScalarFunctionFactory = ZeroArgSingleRowFunction::factory().into();
|
|
let function = factory.provide(crate::function::FunctionContext {
|
|
query_ctx: QueryContext::arc(),
|
|
state: Arc::new(state),
|
|
});
|
|
let args = datafusion_expr::ScalarFunctionArgs {
|
|
args: vec![],
|
|
arg_fields: vec![],
|
|
return_field: Arc::new(Field::new("result", ArrowDataType::UInt64, false)),
|
|
number_rows: 2,
|
|
config_options: Arc::new(datafusion_common::config::ConfigOptions::default()),
|
|
};
|
|
|
|
let err = function
|
|
.as_async()
|
|
.unwrap()
|
|
.invoke_async_with_args(args)
|
|
.await
|
|
.unwrap_err();
|
|
assert!(err.to_string().contains("received 2"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_purge_table_resolves_names_and_returns_zero() {
|
|
let handler = Arc::new(RecordingHandler::default());
|
|
let handler_ref: ProcedureServiceHandlerRef = handler.clone();
|
|
let query_ctx = QueryContextBuilder::default()
|
|
.current_catalog("catalog".to_string())
|
|
.current_schema("schema".to_string())
|
|
.build()
|
|
.into();
|
|
|
|
for (input, expected) in [
|
|
("foo", TableName::new("catalog", "schema", "foo")),
|
|
("other.foo", TableName::new("catalog", "other", "foo")),
|
|
(
|
|
"catalog.other.foo",
|
|
TableName::new("catalog", "other", "foo"),
|
|
),
|
|
] {
|
|
assert_eq!(
|
|
Value::from(0_u64),
|
|
purge_table(&handler_ref, &query_ctx, &[ValueRef::String(input)])
|
|
.await
|
|
.unwrap()
|
|
);
|
|
let calls = handler.calls.lock().unwrap();
|
|
assert_eq!(expected, calls.last().unwrap().0);
|
|
assert!(Arc::ptr_eq(&query_ctx, &calls.last().unwrap().1));
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_purge_table_rejects_invalid_arguments() {
|
|
let recording_handler = Arc::new(RecordingHandler::default());
|
|
let handler: ProcedureServiceHandlerRef = recording_handler.clone();
|
|
for params in [vec![], vec![ValueRef::String("a"), ValueRef::String("b")]] {
|
|
assert!(
|
|
purge_table(&handler, &QueryContext::arc(), ¶ms)
|
|
.await
|
|
.is_err()
|
|
);
|
|
}
|
|
assert!(
|
|
purge_table(&handler, &QueryContext::arc(), &[ValueRef::Int64(1)])
|
|
.await
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
purge_table(&handler, &QueryContext::arc(), &[ValueRef::Null])
|
|
.await
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
purge_table(
|
|
&handler,
|
|
&QueryContext::arc(),
|
|
&[ValueRef::String("a.b.c.d")]
|
|
)
|
|
.await
|
|
.is_err()
|
|
);
|
|
assert!(recording_handler.calls.lock().unwrap().is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_purge_table_propagates_handler_error() {
|
|
let handler: ProcedureServiceHandlerRef = Arc::new(RecordingHandler {
|
|
fail: true,
|
|
..Default::default()
|
|
});
|
|
let error = purge_table(&handler, &QueryContext::arc(), &[ValueRef::String("foo")])
|
|
.await
|
|
.unwrap_err();
|
|
assert!(error.to_string().contains("purge failed"));
|
|
}
|
|
}
|