mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-29 01:29:00 +00:00
0364523dad
* fix(s3select): enforce query and resource limits * fix(s3select): close query resource limit gaps * fix(s3select): preserve timeout and stream invariants * fix(s3select): enforce staged query limits * fix(s3select): preserve policy error compatibility * fix(s3select): bound error source traversal
263 lines
7.8 KiB
Rust
263 lines
7.8 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use datafusion::{common::DataFusionError, sql::sqlparser::parser::ParserError};
|
|
use std::fmt::Display;
|
|
use thiserror::Error;
|
|
|
|
pub mod object_store;
|
|
pub mod query;
|
|
pub mod server;
|
|
mod storage_api;
|
|
|
|
#[cfg(test)]
|
|
mod test;
|
|
|
|
pub type QueryResult<T> = Result<T, QueryError>;
|
|
pub(crate) use storage_api::crate_boundary::{
|
|
SELECT_DEFAULT_READ_BUFFER_SIZE, SelectGetObjectReader, SelectObjectInfo, SelectObjectOptions, SelectStorageError,
|
|
SelectStore, resolve_select_object_store_handle, select_is_err_bucket_not_found, select_is_err_object_not_found,
|
|
select_is_err_version_not_found,
|
|
};
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum QueryError {
|
|
#[error("DataFusion error: {source}")]
|
|
Datafusion { source: Box<DataFusionError> },
|
|
|
|
#[error("This feature is not implemented: {err}")]
|
|
NotImplemented { err: String },
|
|
|
|
#[error("Multi-statement not allow, found num:{num}, sql:{sql}")]
|
|
MultiStatement { num: usize, sql: String },
|
|
|
|
#[error("Failed to build QueryDispatcher. err: {err}")]
|
|
BuildQueryDispatcher { err: String },
|
|
|
|
#[error("The query has been canceled")]
|
|
Cancel,
|
|
|
|
#[error("{source}")]
|
|
Parser {
|
|
#[from]
|
|
source: ParserError,
|
|
},
|
|
|
|
#[error("Udf not exists, name:{name}.")]
|
|
FunctionNotExists { name: String },
|
|
|
|
#[error("Udf already exists, name:{name}.")]
|
|
FunctionExists { name: String },
|
|
|
|
#[error("Store Error, e:{e}.")]
|
|
StoreError { e: String },
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
#[non_exhaustive]
|
|
pub enum S3SelectPolicyError {
|
|
#[error("Unsupported S3 Select SQL structure: {message}")]
|
|
UnsupportedSqlStructure { message: String },
|
|
|
|
#[error("S3 Select query concurrency limit reached")]
|
|
QueryConcurrencyLimit,
|
|
|
|
#[error("S3 Select query exceeded the {seconds}-second execution limit")]
|
|
QueryTimeout { seconds: u64 },
|
|
}
|
|
|
|
impl S3SelectPolicyError {
|
|
fn from_error<'a>(mut err: &'a (dyn std::error::Error + 'static)) -> Option<&'a Self> {
|
|
for _ in 0..16 {
|
|
if let Some(policy_error) = err.downcast_ref::<Self>() {
|
|
return Some(policy_error);
|
|
}
|
|
err = err.source()?;
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
impl QueryError {
|
|
pub fn s3_select_policy_error(&self) -> Option<&S3SelectPolicyError> {
|
|
match self {
|
|
Self::Datafusion { source } => S3SelectPolicyError::from_error(source.as_ref()),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<S3SelectPolicyError> for QueryError {
|
|
fn from(value: S3SelectPolicyError) -> Self {
|
|
Self::Datafusion {
|
|
source: Box::new(DataFusionError::External(Box::new(value))),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<DataFusionError> for QueryError {
|
|
fn from(value: DataFusionError) -> Self {
|
|
match value {
|
|
DataFusionError::External(e) => match e.downcast::<QueryError>() {
|
|
Ok(query_error) => *query_error,
|
|
Err(e) => Self::Datafusion {
|
|
source: Box::new(DataFusionError::External(e)),
|
|
},
|
|
},
|
|
v => Self::Datafusion { source: Box::new(v) },
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ResolvedTable {
|
|
// path
|
|
table: String,
|
|
}
|
|
|
|
impl ResolvedTable {
|
|
pub fn table(&self) -> &str {
|
|
&self.table
|
|
}
|
|
}
|
|
|
|
impl Display for ResolvedTable {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let Self { table } = self;
|
|
write!(f, "{table}")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use datafusion::common::DataFusionError;
|
|
use datafusion::sql::sqlparser::parser::ParserError;
|
|
|
|
#[test]
|
|
fn test_query_error_display() {
|
|
let err = QueryError::NotImplemented {
|
|
err: "feature X".to_string(),
|
|
};
|
|
assert_eq!(err.to_string(), "This feature is not implemented: feature X");
|
|
|
|
let err = QueryError::MultiStatement {
|
|
num: 2,
|
|
sql: "SELECT 1; SELECT 2;".to_string(),
|
|
};
|
|
assert_eq!(err.to_string(), "Multi-statement not allow, found num:2, sql:SELECT 1; SELECT 2;");
|
|
|
|
let err = S3SelectPolicyError::UnsupportedSqlStructure {
|
|
message: "JOIN is not supported".to_string(),
|
|
};
|
|
assert_eq!(err.to_string(), "Unsupported S3 Select SQL structure: JOIN is not supported");
|
|
|
|
let err = QueryError::Cancel;
|
|
assert_eq!(err.to_string(), "The query has been canceled");
|
|
|
|
assert_eq!(
|
|
S3SelectPolicyError::QueryConcurrencyLimit.to_string(),
|
|
"S3 Select query concurrency limit reached"
|
|
);
|
|
assert_eq!(
|
|
S3SelectPolicyError::QueryTimeout { seconds: 300 }.to_string(),
|
|
"S3 Select query exceeded the 300-second execution limit"
|
|
);
|
|
|
|
let err = QueryError::FunctionNotExists {
|
|
name: "my_func".to_string(),
|
|
};
|
|
assert_eq!(err.to_string(), "Udf not exists, name:my_func.");
|
|
|
|
let err = QueryError::StoreError {
|
|
e: "connection failed".to_string(),
|
|
};
|
|
assert_eq!(err.to_string(), "Store Error, e:connection failed.");
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_error_from_datafusion_error() {
|
|
let df_error = DataFusionError::Plan("invalid plan".to_string());
|
|
let query_error: QueryError = df_error.into();
|
|
|
|
match query_error {
|
|
QueryError::Datafusion { source, .. } => {
|
|
assert!(source.to_string().contains("invalid plan"));
|
|
}
|
|
_ => panic!("Expected Datafusion error"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn query_error_variants_remain_source_compatible() {
|
|
fn exhaustive_match(err: QueryError) {
|
|
match err {
|
|
QueryError::Datafusion { .. }
|
|
| QueryError::NotImplemented { .. }
|
|
| QueryError::MultiStatement { .. }
|
|
| QueryError::BuildQueryDispatcher { .. }
|
|
| QueryError::Cancel
|
|
| QueryError::Parser { .. }
|
|
| QueryError::FunctionNotExists { .. }
|
|
| QueryError::FunctionExists { .. }
|
|
| QueryError::StoreError { .. } => {}
|
|
}
|
|
}
|
|
|
|
exhaustive_match(QueryError::Cancel);
|
|
}
|
|
|
|
#[test]
|
|
fn policy_error_is_recoverable_from_query_error() {
|
|
let err: QueryError = S3SelectPolicyError::QueryTimeout { seconds: 300 }.into();
|
|
|
|
assert!(matches!(
|
|
err.s3_select_policy_error(),
|
|
Some(S3SelectPolicyError::QueryTimeout { seconds: 300 })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_query_error_from_parser_error() {
|
|
let parser_error = ParserError::ParserError("syntax error".to_string());
|
|
let query_error = QueryError::Parser { source: parser_error };
|
|
|
|
assert!(query_error.to_string().contains("syntax error"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolved_table() {
|
|
let table = ResolvedTable {
|
|
table: "my_table".to_string(),
|
|
};
|
|
|
|
assert_eq!(table.table(), "my_table");
|
|
assert_eq!(table.to_string(), "my_table");
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolved_table_clone_and_eq() {
|
|
let table1 = ResolvedTable {
|
|
table: "table1".to_string(),
|
|
};
|
|
let table2 = table1.clone();
|
|
let table3 = ResolvedTable {
|
|
table: "table2".to_string(),
|
|
};
|
|
|
|
assert_eq!(table1, table2);
|
|
assert_ne!(table1, table3);
|
|
}
|
|
}
|