mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 11:32:19 +00:00
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:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user