From 97ca104129536075fcd7329a924db8c40ef88d7f Mon Sep 17 00:00:00 2001 From: discord9 Date: Fri, 7 Aug 2026 14:48:15 +0800 Subject: [PATCH] fix(frontend): remove gRPC DDL panics for DropView and non-timestamp time index (#8739) * fix(frontend): remove gRPC DDL panics for DropView and non-timestamp time index Direct gRPC DDL bypasses the SQL parser, so two client-controlled DDL payloads could panic a request handler: - QX-152: DdlExpr::DropView hit todo!() (instance/grpc.rs:247-248). Wire it to the real drop-view implementation (drop_view was pub(crate); widened to pub) so a DropView DDL returns a structured error (e.g. TableNotFound) instead of panicking. - QX-153: a CreateTableExpr whose time_index column is not a timestamp reached Schema::new's unwrap (ddl.rs:2346 -> schema.rs:114-119). create_table_info now uses Schema::try_new with ConvertSchemaSnafu context (InvalidArguments), and the direct gRPC CreateTable arm validates the request via validate_create_expr (which now also checks the time-index column type is a timestamp) before any catalog work. SQL/HTTP paths were already protected by the parser; unchanged. Tests: qx_152_drop_view_via_grpc_ddl_returns_error_not_panic, qx_153_create_table_with_non_timestamp_time_index_via_grpc_returns_error (asserts InvalidArguments), test_create_table_info_rejects_non_timestamp_time_index. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(frontend): add gRPC DDL happy-path coverage for DropView and CreateTable Per review: the initial tests only asserted error paths. Add: - drop_if_exists=true on a missing view succeeds (no error) - dropping an existing view via gRPC DDL succeeds end-to-end - a valid CreateTableExpr with a timestamp time index still succeeds (guards validate_create_expr against rejecting good requests) - qx_152 test now asserts the TableNotFound status instead of is_err() Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --------- Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- src/frontend/src/instance.rs | 527 +++++++++++++++++++++++++++++- src/frontend/src/instance/grpc.rs | 15 +- src/operator/src/expr_helper.rs | 31 +- src/operator/src/statement/ddl.rs | 36 +- 4 files changed, 596 insertions(+), 13 deletions(-) diff --git a/src/frontend/src/instance.rs b/src/frontend/src/instance.rs index 1b9710073d..14acf87787 100644 --- a/src/frontend/src/instance.rs +++ b/src/frontend/src/instance.rs @@ -1691,7 +1691,7 @@ mod tests { use common_meta::cache::LayeredCacheRegistryBuilder; use common_meta::kv_backend::memory::MemoryKvBackend; use common_meta::procedure_executor::{ExecutorContext, ProcedureExecutor}; - use common_meta::rpc::ddl::{SubmitDdlTaskRequest, SubmitDdlTaskResponse}; + use common_meta::rpc::ddl::{DdlTask, SubmitDdlTaskRequest, SubmitDdlTaskResponse}; use common_meta::rpc::procedure::{ MigrateRegionRequest, MigrateRegionResponse, ProcedureStateResponse, }; @@ -1722,7 +1722,9 @@ mod tests { }; use store_api::storage::ScanRequest; use strfmt::Format; - use table::metadata::{FilterPushDownType, TableInfo, TableInfoBuilder, TableMetaBuilder}; + use table::metadata::{ + FilterPushDownType, TableInfo, TableInfoBuilder, TableMetaBuilder, TableType, + }; use table::table_name::TableName; use table::test_util::{EmptyTable, MemTable}; use table::{Table, TableRef}; @@ -2307,6 +2309,139 @@ mod tests { } } + /// A test [`ProcedureExecutor`] that completes create/drop DDL tasks against the + /// in-memory catalog, mimicking what the meta DDL procedures do in production. + /// This allows happy-path DDL requests (create/drop table/view) to be exercised + /// end to end through the gRPC ingress. + struct MockProcedureExecutor { + catalog_manager: Arc, + next_table_id: std::sync::atomic::AtomicU32, + submitted: std::sync::Mutex>, + } + + impl MockProcedureExecutor { + fn new(catalog_manager: Arc) -> Self { + Self { + catalog_manager, + next_table_id: std::sync::atomic::AtomicU32::new(1026), + submitted: std::sync::Mutex::new(Vec::new()), + } + } + } + + #[async_trait::async_trait] + impl ProcedureExecutor for MockProcedureExecutor { + async fn submit_ddl_task( + &self, + _ctx: &ExecutorContext, + request: SubmitDdlTaskRequest, + ) -> common_meta::error::Result { + self.submitted.lock().unwrap().push(request.task.clone()); + match request.task { + DdlTask::CreateTable(task) => { + let table_id = self + .next_table_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let mut table_info = task.table_info; + table_info.ident.table_id = table_id; + self.catalog_manager + .register_table_sync(catalog::RegisterTableRequest { + catalog: table_info.catalog_name.clone(), + schema: table_info.schema_name.clone(), + table_name: table_info.name.clone(), + table_id, + table: table::dist_table::DistTable::table(Arc::new(table_info)), + }) + .map_err(BoxedError::new) + .context(common_meta::error::ExternalSnafu)?; + Ok(SubmitDdlTaskResponse { + key: Vec::new(), + table_ids: vec![table_id], + }) + } + DdlTask::CreateView(task) => { + let view_id = self + .next_table_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let mut view_info = task.view_info; + view_info.ident.table_id = view_id; + self.catalog_manager + .register_table_sync(catalog::RegisterTableRequest { + catalog: task.create_view.catalog_name.clone(), + schema: task.create_view.schema_name.clone(), + table_name: task.create_view.view_name.clone(), + table_id: view_id, + table: table::dist_table::DistTable::table(Arc::new(view_info)), + }) + .map_err(BoxedError::new) + .context(common_meta::error::ExternalSnafu)?; + Ok(SubmitDdlTaskResponse { + key: Vec::new(), + table_ids: vec![view_id], + }) + } + DdlTask::DropView(task) => { + self.catalog_manager + .deregister_table_sync(catalog::DeregisterTableRequest { + catalog: task.catalog.clone(), + schema: task.schema.clone(), + table_name: task.view.clone(), + }) + .map_err(BoxedError::new) + .context(common_meta::error::ExternalSnafu)?; + Ok(SubmitDdlTaskResponse::default()) + } + other => common_meta::error::UnsupportedSnafu { + operation: format!("mock submit_ddl_task: {other:?}"), + } + .fail(), + } + } + + async fn migrate_region( + &self, + _ctx: &ExecutorContext, + _request: MigrateRegionRequest, + ) -> common_meta::error::Result { + common_meta::error::UnsupportedSnafu { + operation: "migrate_region", + } + .fail() + } + + async fn reconcile( + &self, + _ctx: &ExecutorContext, + _request: ReconcileRequest, + ) -> common_meta::error::Result { + common_meta::error::UnsupportedSnafu { + operation: "reconcile", + } + .fail() + } + + async fn query_procedure_state( + &self, + _ctx: &ExecutorContext, + _pid: &str, + ) -> common_meta::error::Result { + common_meta::error::UnsupportedSnafu { + operation: "query_procedure_state", + } + .fail() + } + + async fn list_procedures( + &self, + _ctx: &ExecutorContext, + ) -> common_meta::error::Result { + common_meta::error::UnsupportedSnafu { + operation: "list_procedures", + } + .fail() + } + } + fn test_cache_registry( kv_backend: common_meta::kv_backend::KvBackendRef, ) -> TestResult { @@ -2485,10 +2620,29 @@ mod tests { target_table: TableRef, plugins: Plugins, metric_names_table: Option, + ) -> TestResult { + let catalog_manager = catalog::memory::MemoryCatalogManager::new_with_table(source_table); + test_instance_with_catalog_manager( + catalog_manager, + target_table, + plugins, + metric_names_table, + Arc::new(NoopProcedureExecutor), + ) + .await + } + + /// Builds a test frontend `Instance` over the given (already source-registered) + /// catalog manager, completing DDL tasks through `procedure_executor`. + async fn test_instance_with_catalog_manager( + catalog_manager: Arc, + target_table: TableRef, + plugins: Plugins, + metric_names_table: Option, + procedure_executor: ProcedureExecutorRef, ) -> TestResult { let kv_backend = Arc::new(MemoryKvBackend::new()); let process_manager = Arc::new(ProcessManager::new("test-frontend".to_string(), None)); - let catalog_manager = catalog::memory::MemoryCatalogManager::new_with_table(source_table); let target_table_name = "target"; catalog_manager .register_table_sync(catalog::RegisterTableRequest { @@ -2529,7 +2683,7 @@ mod tests { cache_registry, catalog_manager, Arc::new(client::client_manager::NodeClients::default()), - Arc::new(NoopProcedureExecutor), + procedure_executor, process_manager, ) .with_plugin(plugins) @@ -3277,4 +3431,369 @@ mod tests { assert_eq!(result.is_ok(), is_ok); } } + + /// A `DropView` DDL sent through the direct gRPC ingress must return an error + /// (e.g. table not found) instead of panicking on `todo!()`. + #[tokio::test] + async fn qx_152_drop_view_via_grpc_ddl_returns_error_not_panic() -> TestResult<()> { + let instance = + test_instance_with_tables(test_table(1024, "source")?, test_table(1025, "target")?) + .await?; + + let request = api::v1::greptime_request::Request::Ddl(api::v1::DdlRequest { + expr: Some(api::v1::ddl_request::Expr::DropView( + api::v1::DropViewExpr { + catalog_name: String::new(), + schema_name: String::new(), + view_name: "non_existent_view".to_string(), + view_id: None, + drop_if_exists: false, + }, + )), + }); + + let result = servers::query_handler::grpc::GrpcQueryHandler::do_query( + &instance, + request, + QueryContext::arc(), + ) + .await; + + let err = match result { + Ok(_) => panic!("DropView DDL request must return an error instead of panicking"), + Err(err) => err, + }; + assert_eq!( + err.status_code(), + StatusCode::TableNotFound, + "dropping a non-existent view without IF EXISTS must report TableNotFound, got {err}" + ); + Ok(()) + } + + /// `DROP VIEW IF EXISTS` on a missing view through the direct gRPC ingress must + /// succeed with 0 affected rows (no error, no DDL task submitted), instead of + /// returning `TableNotFound`. + #[tokio::test] + async fn qx_152_drop_view_if_exists_missing_view_via_grpc_ddl_succeeds() -> TestResult<()> { + let catalog_manager = + catalog::memory::MemoryCatalogManager::new_with_table(test_table(1024, "source")?); + let procedure_executor = Arc::new(MockProcedureExecutor::new(catalog_manager.clone())); + let instance = test_instance_with_catalog_manager( + catalog_manager, + test_table(1025, "target")?, + Plugins::new(), + None, + procedure_executor.clone() as ProcedureExecutorRef, + ) + .await?; + + let request = api::v1::greptime_request::Request::Ddl(api::v1::DdlRequest { + expr: Some(api::v1::ddl_request::Expr::DropView( + api::v1::DropViewExpr { + catalog_name: String::new(), + schema_name: String::new(), + view_name: "non_existent_view".to_string(), + view_id: None, + drop_if_exists: true, + }, + )), + }); + + let result = servers::query_handler::grpc::GrpcQueryHandler::do_query( + &instance, + request, + QueryContext::arc(), + ) + .await; + + let output = match result { + Ok(output) => output, + Err(err) => { + panic!("DROP VIEW IF EXISTS on a missing view must succeed, got error: {err}") + } + }; + assert!( + matches!(output.data, OutputData::AffectedRows(0)), + "DROP VIEW IF EXISTS on a missing view must report 0 affected rows" + ); + assert!( + procedure_executor.submitted.lock().unwrap().is_empty(), + "DROP VIEW IF EXISTS on a missing view must not submit a DDL task" + ); + Ok(()) + } + + /// A `CREATE VIEW` followed by `DROP VIEW` through the direct gRPC ingress must + /// succeed end to end: the view is registered in the catalog and then removed. + #[tokio::test] + async fn qx_152_drop_existing_view_via_grpc_ddl_succeeds() -> TestResult<()> { + let catalog_manager = + catalog::memory::MemoryCatalogManager::new_with_table(test_table(1024, "source")?); + let procedure_executor = Arc::new(MockProcedureExecutor::new(catalog_manager.clone())); + let instance = test_instance_with_catalog_manager( + catalog_manager, + test_table(1025, "target")?, + Plugins::new(), + None, + procedure_executor.clone() as ProcedureExecutorRef, + ) + .await?; + + // The default "greptime.public" schema must be visible to the kv-backed table + // metadata manager for `CREATE VIEW`/`CREATE TABLE` to pass validation. + instance + .table_metadata_manager() + .schema_manager() + .create( + common_meta::key::schema_name::SchemaNameKey::new("greptime", "public"), + None, + true, + ) + .await + .unwrap(); + + let create_view_request = api::v1::greptime_request::Request::Ddl(api::v1::DdlRequest { + expr: Some(api::v1::ddl_request::Expr::CreateView( + api::v1::CreateViewExpr { + catalog_name: String::new(), + schema_name: String::new(), + view_name: "my_view".to_string(), + logical_plan: vec![1, 2, 3], + create_if_not_exists: false, + or_replace: false, + table_names: vec![], + columns: vec![], + plan_columns: vec![], + definition: "CREATE VIEW my_view AS SELECT * FROM source".to_string(), + }, + )), + }); + + let output = match servers::query_handler::grpc::GrpcQueryHandler::do_query( + &instance, + create_view_request, + QueryContext::arc(), + ) + .await + { + Ok(output) => output, + Err(err) => panic!("CREATE VIEW via gRPC DDL must succeed, got error: {err}"), + }; + assert!( + matches!(output.data, OutputData::AffectedRows(0)), + "CREATE VIEW via gRPC DDL must report 0 affected rows" + ); + + // The view is registered in the catalog as a view. + let view = instance + .catalog_manager() + .table("greptime", "public", "my_view", None) + .await + .unwrap() + .expect("view should exist after CREATE VIEW"); + assert_eq!(view.table_info().table_type, TableType::View); + + let drop_view_request = api::v1::greptime_request::Request::Ddl(api::v1::DdlRequest { + expr: Some(api::v1::ddl_request::Expr::DropView( + api::v1::DropViewExpr { + catalog_name: String::new(), + schema_name: String::new(), + view_name: "my_view".to_string(), + view_id: None, + drop_if_exists: false, + }, + )), + }); + + let output = match servers::query_handler::grpc::GrpcQueryHandler::do_query( + &instance, + drop_view_request, + QueryContext::arc(), + ) + .await + { + Ok(output) => output, + Err(err) => panic!("DROP VIEW via gRPC DDL must succeed, got error: {err}"), + }; + assert!( + matches!(output.data, OutputData::AffectedRows(0)), + "DROP VIEW via gRPC DDL must report 0 affected rows" + ); + + // The view is gone after the drop. + assert!( + instance + .catalog_manager() + .table("greptime", "public", "my_view", None) + .await + .unwrap() + .is_none(), + "view should be removed after DROP VIEW" + ); + + let submitted = procedure_executor.submitted.lock().unwrap(); + assert_eq!( + submitted.len(), + 2, + "expected create and drop view tasks, got {submitted:?}" + ); + assert!(matches!(&submitted[0], DdlTask::CreateView(_))); + assert!(matches!(&submitted[1], DdlTask::DropView(_))); + Ok(()) + } + + /// A direct gRPC `CreateTable` whose time index column is not a timestamp must + /// be rejected with `InvalidArguments` instead of panicking while building the schema. + #[tokio::test] + async fn qx_153_create_table_with_non_timestamp_time_index_via_grpc_returns_error() + -> TestResult<()> { + let instance = + test_instance_with_tables(test_table(1024, "source")?, test_table(1025, "target")?) + .await?; + + let request = api::v1::greptime_request::Request::Ddl(api::v1::DdlRequest { + expr: Some(api::v1::ddl_request::Expr::CreateTable( + api::v1::CreateTableExpr { + catalog_name: String::new(), + schema_name: String::new(), + table_name: "demo".to_string(), + desc: String::new(), + column_defs: vec![api::v1::ColumnDef { + name: "host".to_string(), + data_type: api::v1::ColumnDataType::String as i32, + is_nullable: true, + default_constraint: vec![], + semantic_type: 0, + comment: String::new(), + datatype_extension: None, + options: None, + }], + time_index: "host".to_string(), + primary_keys: vec![], + create_if_not_exists: false, + table_options: HashMap::new(), + table_id: None, + engine: "mito".to_string(), + }, + )), + }); + + let result = servers::query_handler::grpc::GrpcQueryHandler::do_query( + &instance, + request, + QueryContext::arc(), + ) + .await; + + let err = match result { + Ok(_) => panic!("CreateTable with a non-timestamp time index must be rejected"), + Err(err) => err, + }; + assert_eq!(err.status_code(), StatusCode::InvalidArguments, "{err}"); + Ok(()) + } + + /// A valid `CREATE TABLE` (timestamp time index) through the direct gRPC ingress + /// must succeed, guarding that the `validate_create_expr` ingress check doesn't + /// accidentally reject good requests. + #[tokio::test] + async fn qx_153_create_table_with_timestamp_time_index_via_grpc_succeeds() -> TestResult<()> { + let catalog_manager = + catalog::memory::MemoryCatalogManager::new_with_table(test_table(1024, "source")?); + let procedure_executor = Arc::new(MockProcedureExecutor::new(catalog_manager.clone())); + let instance = test_instance_with_catalog_manager( + catalog_manager, + test_table(1025, "target")?, + Plugins::new(), + None, + procedure_executor.clone() as ProcedureExecutorRef, + ) + .await?; + + // The default "greptime.public" schema must be visible to the kv-backed table + // metadata manager for `CREATE TABLE` to pass validation. + instance + .table_metadata_manager() + .schema_manager() + .create( + common_meta::key::schema_name::SchemaNameKey::new("greptime", "public"), + None, + true, + ) + .await + .unwrap(); + + let request = api::v1::greptime_request::Request::Ddl(api::v1::DdlRequest { + expr: Some(api::v1::ddl_request::Expr::CreateTable( + api::v1::CreateTableExpr { + catalog_name: String::new(), + schema_name: String::new(), + table_name: "demo".to_string(), + desc: String::new(), + column_defs: vec![ + api::v1::ColumnDef { + name: "host".to_string(), + data_type: api::v1::ColumnDataType::String as i32, + is_nullable: true, + default_constraint: vec![], + semantic_type: 0, + comment: String::new(), + datatype_extension: None, + options: None, + }, + api::v1::ColumnDef { + name: "ts".to_string(), + data_type: api::v1::ColumnDataType::TimestampMillisecond as i32, + is_nullable: true, + default_constraint: vec![], + semantic_type: 0, + comment: String::new(), + datatype_extension: None, + options: None, + }, + ], + time_index: "ts".to_string(), + primary_keys: vec![], + create_if_not_exists: false, + table_options: HashMap::new(), + table_id: None, + engine: "mito".to_string(), + }, + )), + }); + + let output = match servers::query_handler::grpc::GrpcQueryHandler::do_query( + &instance, + request, + QueryContext::arc(), + ) + .await + { + Ok(output) => output, + Err(err) => panic!("CREATE TABLE via gRPC DDL must succeed, got error: {err}"), + }; + assert!( + matches!(output.data, OutputData::AffectedRows(0)), + "CREATE TABLE via gRPC DDL must report 0 affected rows" + ); + + // The table is registered in the catalog. + let table = instance + .catalog_manager() + .table("greptime", "public", "demo", None) + .await + .unwrap() + .expect("table should exist after CREATE TABLE"); + assert_eq!(table.table_info().table_type, TableType::Base); + + let submitted = procedure_executor.submitted.lock().unwrap(); + assert_eq!( + submitted.len(), + 1, + "expected one create table task, got {submitted:?}" + ); + assert!(matches!(&submitted[0], DdlTask::CreateTable(_))); + Ok(()) + } } diff --git a/src/frontend/src/instance/grpc.rs b/src/frontend/src/instance/grpc.rs index 661afb7f43..3caac10b84 100644 --- a/src/frontend/src/instance/grpc.rs +++ b/src/frontend/src/instance/grpc.rs @@ -176,6 +176,9 @@ impl GrpcQueryHandler for Instance { match expr { DdlExpr::CreateTable(mut expr) => { + // Direct gRPC DDL bypasses the SQL parser, so validate the + // request here (e.g. the time index must be a timestamp). + operator::expr_helper::validate_create_expr(&expr)?; let _ = self .statement_executor .create_table_inner(&mut expr, None, ctx.clone()) @@ -244,8 +247,16 @@ impl GrpcQueryHandler for Instance { Output::new_with_affected_rows(0) } - DdlExpr::DropView(_) => { - todo!("implemented in the following PR") + DdlExpr::DropView(expr) => { + self.statement_executor + .drop_view( + expr.catalog_name, + expr.schema_name, + expr.view_name, + expr.drop_if_exists, + ctx.clone(), + ) + .await? } DdlExpr::CommentOn(expr) => { self.statement_executor diff --git a/src/operator/src/expr_helper.rs b/src/operator/src/expr_helper.rs index 45e3e340a3..3221598cf2 100644 --- a/src/operator/src/expr_helper.rs +++ b/src/operator/src/expr_helper.rs @@ -36,6 +36,7 @@ use common_error::ext::BoxedError; use common_grpc_expr::util::ColumnExpr; use common_time::Timezone; use datafusion::sql::planner::object_name_to_table_reference; +use datatypes::prelude::ConcreteDataType; use datatypes::schema::{ COLUMN_FULLTEXT_OPT_KEY_ANALYZER, COLUMN_FULLTEXT_OPT_KEY_BACKEND, COLUMN_FULLTEXT_OPT_KEY_CASE_SENSITIVE, COLUMN_FULLTEXT_OPT_KEY_FALSE_POSITIVE_RATE, @@ -442,14 +443,34 @@ pub fn validate_create_expr(create: &CreateTableExpr) -> Result<()> { } // verify time_index exists - let _ = column_to_indices - .get(&create.time_index) - .with_context(|| InvalidSqlSnafu { + let time_index_idx = + column_to_indices + .get(&create.time_index) + .with_context(|| InvalidSqlSnafu { + err_msg: format!( + "column name `{}` is not found in column list", + create.time_index + ), + })?; + + // verify time_index is a timestamp column + let time_index_column = &create.column_defs[*time_index_idx]; + let data_type = ConcreteDataType::from( + ColumnDataTypeWrapper::try_new( + time_index_column.data_type, + time_index_column.datatype_extension.clone(), + ) + .context(ColumnDataTypeSnafu)?, + ); + ensure!( + data_type.is_timestamp(), + InvalidSqlSnafu { err_msg: format!( - "column name `{}` is not found in column list", + "column `{}` is not a timestamp type, it can't be used as time index", create.time_index ), - })?; + } + ); // verify primary_key exists for pk in &create.primary_keys { diff --git a/src/operator/src/statement/ddl.rs b/src/operator/src/statement/ddl.rs index b0986de59f..96321b13a3 100644 --- a/src/operator/src/statement/ddl.rs +++ b/src/operator/src/statement/ddl.rs @@ -1213,7 +1213,7 @@ impl StatementExecutor { /// Drop a view #[tracing::instrument(skip_all)] - pub(crate) async fn drop_view( + pub async fn drop_view( &self, catalog: String, schema: String, @@ -2343,7 +2343,7 @@ pub fn create_table_info( } let next_column_id = column_schemas.len() as u32; - let schema = Arc::new(Schema::new(column_schemas)); + let schema = Arc::new(Schema::try_new(column_schemas).context(ConvertSchemaSnafu)?); let primary_key_indices = create_table .primary_keys @@ -3443,6 +3443,38 @@ WITH ('repartition.column.hint' = ' host ')", ); } + #[test] + fn test_create_table_info_rejects_non_timestamp_time_index() { + let expr = CreateTableExpr { + catalog_name: "greptime".to_string(), + schema_name: "public".to_string(), + table_name: "demo".to_string(), + desc: String::new(), + column_defs: vec![api::v1::ColumnDef { + name: "host".to_string(), + data_type: api::v1::ColumnDataType::String as i32, + is_nullable: true, + default_constraint: vec![], + semantic_type: 0, + comment: String::new(), + datatype_extension: None, + options: None, + }], + time_index: "host".to_string(), + primary_keys: vec![], + create_if_not_exists: false, + table_options: HashMap::new(), + table_id: None, + engine: "mito".to_string(), + }; + + let err = create_table_info(&expr, vec![]).unwrap_err(); + assert_eq!( + common_error::ext::ErrorExt::status_code(&err), + common_error::status_code::StatusCode::InvalidArguments + ); + } + #[test] fn test_json2_requires_append_mode() { let cases = [