Files
rustfs/crates/s3select-api/src/lib.rs
T
GatewayJ 0364523dad fix(s3select): enforce query and resource limits (#5028)
* 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
2026-07-25 18:44:53 +08:00

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);
}
}