fix: preserve remote projection column order

This commit is contained in:
Gatefixer
2026-09-23 18:27:56 +00:00
parent df7bfcdb65
commit 55ad434a10
3 changed files with 126 additions and 6 deletions
Generated
+1
View File
@@ -9524,6 +9524,7 @@ version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"indexmap 2.14.0",
"itoa",
"memchr",
"serde",
+1 -1
View File
@@ -72,7 +72,7 @@ oauth2 = { version = "5.0", default-features = false }
regex = "1.10"
semver = "1.0.25"
serde = "1"
serde_json = "1"
serde_json = { version = "1", features = ["preserve_order"] }
tempfile = "3.5.0"
tokio = { version = "1.23", features = ["rt-multi-thread", "sync"] }
tonic = { version = "0.14", features = ["tls-native-roots", "tls-ring"] }
+124 -5
View File
@@ -1046,6 +1046,8 @@ impl<S: HttpSend> RemoteTable<S> {
);
}
Select::Dynamic(pairs) => {
// Keep the caller's alias order in the JSON object sent to the server.
// serde_json's preserve_order feature is required here.
let alias_map =
serde_json::Map::from_iter(pairs.iter().map(|(name, expr)| {
(name.clone(), serde_json::Value::String(expr.clone()))
@@ -1387,24 +1389,29 @@ impl<S: HttpSend> RemoteTable<S> {
});
let streams = futures::future::try_join_all(futures);
if let Some(timeout) = options.timeout {
let streams = if let Some(timeout) = options.timeout {
let timeout_future = tokio::time::sleep(timeout);
tokio::pin!(timeout_future);
tokio::pin!(streams);
tokio::select! {
_ = &mut timeout_future => {
Err(Error::Other {
return Err(Error::Other {
message: format!("Query timeout after {} ms", timeout.as_millis()),
source: None,
})
}
result = &mut streams => {
Ok(result?)
result?
}
}
} else {
Ok(streams.await?)
}
streams.await?
};
Ok(streams
.into_iter()
.map(|stream| restore_projection_order(stream, &query.base().select))
.collect())
}
fn prepare_query_bodies(
@@ -1446,6 +1453,52 @@ impl<S: HttpSend> RemoteTable<S> {
}
}
fn restore_projection_order(
stream: Pin<Box<dyn RecordBatchStream + Send>>,
select: &Select,
) -> Pin<Box<dyn RecordBatchStream + Send>> {
let pairs = match select {
Select::Dynamic(pairs) => pairs.iter().map(|(name, _)| name).collect::<Vec<_>>(),
Select::Expr(pairs) => pairs.iter().map(|(name, _)| name).collect::<Vec<_>>(),
_ => return stream,
};
let schema = stream.schema();
let mut remaining = (0..schema.fields().len()).collect::<Vec<_>>();
let mut indices = Vec::with_capacity(remaining.len());
for name in pairs {
if let Some(position) = remaining
.iter()
.position(|&index| schema.field(index).name() == name)
{
indices.push(remaining.remove(position));
}
}
indices.extend(remaining);
if indices
.iter()
.enumerate()
.all(|(index, &field)| index == field)
{
return stream;
}
// Older servers may sort alias maps even when the request preserves their
// order. Reorder the schema and every batch while keeping server-added fields.
let fields = indices
.iter()
.map(|&index| schema.field(index).clone())
.collect::<Vec<_>>();
let schema = Arc::new(arrow_schema::Schema::new_with_metadata(
fields,
schema.metadata().clone(),
));
let batches = stream.map(move |batch| {
batch.and_then(|batch| batch.project(&indices).map_err(DataFusionError::from))
});
Box::pin(RecordBatchStreamAdapter::new(schema, batches))
}
#[derive(Deserialize)]
struct TableDescription {
version: u64,
@@ -5809,6 +5862,72 @@ mod tests {
.unwrap();
}
#[rstest]
#[case::sql(Select::dynamic(&[("p", "x"), ("id", "id")]))]
#[case::typed(Select::expr_projection(&[
("p", crate::expr::col("x")),
("id", crate::expr::col("id")),
]))]
#[tokio::test]
async fn test_query_dynamic_projection_order(#[case] select: Select) {
let table = Table::new_with_handler("my_table", |request| {
assert_eq!(request.url().path(), "/v1/table/my_table/query/");
let body: serde_json::Value =
serde_json::from_slice(request.body().unwrap().as_bytes().unwrap()).unwrap();
let aliases = body["columns"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect::<Vec<_>>();
assert_eq!(aliases, ["p", "id"]);
// Simulate a server that returns aliases in alphabetical order.
let data = RecordBatch::try_new(
Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("p", DataType::Int32, false),
])),
vec![
Arc::new(Int32Array::from(vec![7])),
Arc::new(Int32Array::from(vec![42])),
],
)
.unwrap();
http::Response::builder()
.status(200)
.header(CONTENT_TYPE, ARROW_FILE_CONTENT_TYPE)
.body(write_ipc_file(&data))
.unwrap()
});
let batches = table
.query()
.select(select)
.execute()
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
assert_eq!(batches.len(), 1);
let schema = batches[0].schema();
assert_eq!(
schema
.fields()
.iter()
.map(|field| field.name().as_str())
.collect::<Vec<_>>(),
["p", "id"]
);
let projected = batches[0]
.column(0)
.as_any()
.downcast_ref::<Int32Array>()
.unwrap();
assert_eq!(projected.value(0), 42);
}
#[tokio::test]
async fn test_query_vector_nested_field_path() {
let expected_data = RecordBatch::try_new(