feat(s3select): improve SelectObjectContent streaming (#3072)

* feat(s3select): improve SelectObjectContent streaming

* fix(s3select): reject empty select expressions

* fix(s3select): address streaming review feedback

---------

Co-authored-by: loverustfs <hello@rustfs.com>
This commit is contained in:
GatewayJ
2026-05-24 22:23:47 +08:00
committed by GitHub
parent d74e6eb042
commit b0646be756
9 changed files with 1703 additions and 142 deletions
@@ -31,6 +31,7 @@ use datafusion::{
},
error::Result as DFResult,
execution::{RecordBatchStream, SendableRecordBatchStream},
sql::sqlparser::parser::ParserError,
};
use futures::{Stream, StreamExt};
use rustfs_s3select_api::{
@@ -106,7 +107,11 @@ impl QueryDispatcher for SimpleQueryDispatcher {
let stmt = match statements.front() {
Some(stmt) => stmt.clone(),
None => return Ok(None),
None => {
return Err(QueryError::Parser {
source: ParserError::ParserError("empty SQL expression".to_string()),
});
}
};
let logical_plan = self
@@ -178,15 +178,10 @@ mod error_handling_tests {
let query = Query::new(Context { input: Arc::new(input) }, sql.to_string());
let result = db.execute(&query).await;
// Empty queries might be handled differently by the parser
match result {
Ok(_) => {
// Some parsers might accept empty queries
}
Err(_) => {
// Expected to fail for empty SQL
}
}
assert!(
matches!(result, Err(QueryError::Parser { .. })),
"Expected parser error for empty SQL: {sql:?}"
);
}
}
@@ -21,7 +21,7 @@ mod integration_tests {
};
use s3s::dto::{
CSVInput, CSVOutput, ExpressionType, FileHeaderInfo, InputSerialization, JSONInput, JSONOutput, JSONType,
OutputSerialization, SelectObjectContentInput, SelectObjectContentRequest,
OutputSerialization, ParquetInput, SelectObjectContentInput, SelectObjectContentRequest,
};
use std::sync::Arc;
@@ -83,6 +83,31 @@ mod integration_tests {
}
}
fn create_test_parquet_input(sql: &str) -> SelectObjectContentInput {
SelectObjectContentInput {
bucket: "test-bucket".to_string(),
expected_bucket_owner: None,
key: "test.parquet".to_string(),
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
request: SelectObjectContentRequest {
expression: sql.to_string(),
expression_type: ExpressionType::from_static("SQL"),
input_serialization: InputSerialization {
parquet: Some(ParquetInput {}),
..Default::default()
},
output_serialization: OutputSerialization {
json: Some(JSONOutput::default()),
..Default::default()
},
request_progress: None,
scan_range: None,
},
}
}
#[tokio::test]
async fn test_database_creation() {
let input = create_test_input("SELECT * FROM S3Object");
@@ -290,6 +315,21 @@ mod integration_tests {
assert!(output.is_ok());
}
#[tokio::test]
async fn test_simple_select_query_parquet() {
let sql = "SELECT name, age FROM S3Object WHERE age > 25";
let input = create_test_parquet_input(sql);
let db = get_global_db(input.clone(), true).await.unwrap();
let query = Query::new(Context { input: Arc::new(input) }, sql.to_string());
let result = db.execute(&query).await;
assert!(result.is_ok());
let output = result.unwrap().result().chunk_result().await.unwrap();
let total_rows: usize = output.iter().map(|batch| batch.num_rows()).sum();
assert_eq!(total_rows, 3);
}
#[tokio::test]
async fn test_select_with_where_clause_json() {
let sql = "SELECT name, age FROM S3Object WHERE age > 30";