You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by pa...@apache.org on 2026/09/23 17:36:22 UTC
(sedona-db) branch main updated: fix(rust/sedona-query-planner): reject unsupported aggregate ordering (#1335)
This is an automated email from the ASF dual-hosted git repository.
paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new a1128eefb fix(rust/sedona-query-planner): reject unsupported aggregate ordering (#1335)
a1128eefb is described below
commit a1128eefbec763b7c6ee7becc113426ea4da2dff
Author: Dewey Dunnington <de...@dunnington.ca>
AuthorDate: Wed Sep 23 17:36:15 2026 +0000
fix(rust/sedona-query-planner): reject unsupported aggregate ordering (#1335)
---
c/sedona-geos/src/register.rs | 13 +++-
c/sedona-s2geography/src/register.rs | 14 +++-
c/sedona-s2geography/tests/bounding_functions.rs | 9 ++-
rust/sedona-expr/src/aggregate_udf.rs | 37 ++++++++++
rust/sedona-expr/src/function_set.rs | 43 ++++++++++-
rust/sedona-functions/src/st_analyze_agg.rs | 4 +-
rust/sedona-functions/src/st_envelope_agg.rs | 6 +-
rust/sedona-geo/benches/geo-functions.rs | 4 +-
rust/sedona-geo/src/register.rs | 13 +++-
rust/sedona-query-planner/src/optimizer.rs | 46 +++++++++++-
rust/sedona/src/context.rs | 92 +++++++++++++++++++++---
11 files changed, 254 insertions(+), 27 deletions(-)
diff --git a/c/sedona-geos/src/register.rs b/c/sedona-geos/src/register.rs
index f28e2a131..90ee56ba9 100644
--- a/c/sedona-geos/src/register.rs
+++ b/c/sedona-geos/src/register.rs
@@ -14,7 +14,8 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-use sedona_expr::aggregate_udf::SedonaAccumulatorRef;
+use datafusion_expr::utils::AggregateOrderSensitivity;
+use sedona_expr::aggregate_udf::{SedonaAccumulatorRef, SedonaAggregateUDF};
use sedona_expr::scalar_udf::{IntoScalarKernelRefs, ScalarKernelRef};
macro_rules! define_scalar_kernels {
@@ -102,3 +103,13 @@ pub fn aggregate_kernels() -> Vec<(&'static str, Vec<SedonaAccumulatorRef>)> {
"st_polygonize_agg" => crate::st_polygonize_agg::st_polygonize_agg_impl,
)
}
+
+pub fn aggregate_udfs() -> Vec<SedonaAggregateUDF> {
+ aggregate_kernels()
+ .into_iter()
+ .map(|(name, kernels)| {
+ SedonaAggregateUDF::from_impl(name, kernels)
+ .with_order_sensitivity(AggregateOrderSensitivity::Insensitive)
+ })
+ .collect()
+}
diff --git a/c/sedona-s2geography/src/register.rs b/c/sedona-s2geography/src/register.rs
index 16212f438..0001ecd83 100644
--- a/c/sedona-s2geography/src/register.rs
+++ b/c/sedona-s2geography/src/register.rs
@@ -16,8 +16,9 @@
// under the License.
use datafusion_common::Result;
+use datafusion_expr::utils::AggregateOrderSensitivity;
use sedona_common::sedona_internal_err;
-use sedona_expr::aggregate_udf::SedonaAccumulatorRef;
+use sedona_expr::aggregate_udf::{SedonaAccumulatorRef, SedonaAggregateUDF};
use sedona_expr::item_crs::ItemCrsSedonaAccumulator;
use sedona_expr::scalar_udf::ScalarKernelRef;
use sedona_functions::{st_analyze_agg, st_envelope_agg};
@@ -67,3 +68,14 @@ pub fn aggregate_kernels() -> Vec<(&'static str, Vec<SedonaAccumulatorRef>)> {
),
]
}
+
+/// Returns aggregate UDF declarations for s2geography functions
+pub fn aggregate_udfs() -> Vec<SedonaAggregateUDF> {
+ aggregate_kernels()
+ .into_iter()
+ .map(|(name, kernels)| {
+ SedonaAggregateUDF::from_impl(name, kernels)
+ .with_order_sensitivity(AggregateOrderSensitivity::Insensitive)
+ })
+ .collect()
+}
diff --git a/c/sedona-s2geography/tests/bounding_functions.rs b/c/sedona-s2geography/tests/bounding_functions.rs
index bd06a5f49..7c97e9b47 100644
--- a/c/sedona-s2geography/tests/bounding_functions.rs
+++ b/c/sedona-s2geography/tests/bounding_functions.rs
@@ -17,7 +17,7 @@
use std::sync::Arc;
-use datafusion_expr::Volatility;
+use datafusion_expr::AggregateUDFImpl;
use sedona_expr::{aggregate_udf::SedonaAggregateUDF, scalar_udf::SedonaScalarUDF};
use sedona_geometry::types::Edges;
use sedona_schema::datatypes::SedonaType;
@@ -36,11 +36,10 @@ fn scalar_tester(udf: SedonaScalarUDF, arg_type: SedonaType) -> ScalarUdfTester
}
fn aggregate_udf(name: &str) -> SedonaAggregateUDF {
- let kernels = sedona_s2geography::register::aggregate_kernels()
+ sedona_s2geography::register::aggregate_udfs()
.into_iter()
- .find_map(|(kernel_name, kernels)| (kernel_name == name).then_some(kernels))
- .unwrap_or_else(|| panic!("aggregate kernel not found: {name}"));
- SedonaAggregateUDF::new(name, kernels, Volatility::Immutable)
+ .find(|udf| udf.name() == name)
+ .unwrap_or_else(|| panic!("aggregate UDF not found: {name}"))
}
mod st_envelope {
diff --git a/rust/sedona-expr/src/aggregate_udf.rs b/rust/sedona-expr/src/aggregate_udf.rs
index cb641cb23..82d989c81 100644
--- a/rust/sedona-expr/src/aggregate_udf.rs
+++ b/rust/sedona-expr/src/aggregate_udf.rs
@@ -21,6 +21,7 @@ use datafusion_common::{Result, not_impl_err};
use datafusion_expr::{
Accumulator, AggregateUDFImpl, Documentation, GroupsAccumulator, Signature, Volatility,
function::{AccumulatorArgs, StateFieldsArgs},
+ utils::AggregateOrderSensitivity,
};
use sedona_common::sedona_internal_err;
use sedona_schema::datatypes::SedonaType;
@@ -68,6 +69,7 @@ pub struct SedonaAggregateUDF {
name: String,
signature: Signature,
kernels: Vec<SedonaAccumulatorRef>,
+ order_sensitivity: AggregateOrderSensitivity,
}
impl PartialEq for SedonaAggregateUDF {
@@ -96,6 +98,7 @@ impl SedonaAggregateUDF {
name: name.to_string(),
signature,
kernels: kernels.into_sedona_accumulator_refs(),
+ order_sensitivity: AggregateOrderSensitivity::HardRequirement,
}
}
@@ -104,6 +107,21 @@ impl SedonaAggregateUDF {
Self::new(name, kernels, Volatility::Immutable)
}
+ /// Set how this aggregate responds to an `ORDER BY` clause.
+ ///
+ /// Aggregates default to [`AggregateOrderSensitivity::HardRequirement`] so
+ /// that new and externally registered aggregates are rejected unless they
+ /// explicitly declare that they can safely ignore ordering.
+ pub fn with_order_sensitivity(mut self, order_sensitivity: AggregateOrderSensitivity) -> Self {
+ self.order_sensitivity = order_sensitivity;
+ self
+ }
+
+ /// Return how this aggregate responds to an `ORDER BY` clause.
+ pub fn order_sensitivity(&self) -> AggregateOrderSensitivity {
+ self.order_sensitivity
+ }
+
/// Add a new kernel to an Aggregate UDF
///
/// Because kernels are resolved in reverse order, the new kernel will take
@@ -216,6 +234,10 @@ impl AggregateUDFImpl for SedonaAggregateUDF {
fn documentation(&self) -> Option<&Documentation> {
None
}
+
+ fn order_sensitivity(&self) -> AggregateOrderSensitivity {
+ self.order_sensitivity()
+ }
}
pub trait SedonaAccumulator: Debug + Send + Sync {
@@ -272,6 +294,10 @@ mod test {
Volatility::Immutable,
);
assert_eq!(udf.name(), "empty");
+ assert_eq!(
+ udf.order_sensitivity(),
+ AggregateOrderSensitivity::HardRequirement
+ );
let err = udf.return_field(&[]).unwrap_err();
assert_eq!(err.message(), "empty(): No kernel matching arguments");
assert!(udf.kernels().is_empty());
@@ -282,4 +308,15 @@ mod test {
Ok(())
}
+
+ #[test]
+ fn udaf_order_sensitivity() {
+ let udf = SedonaAggregateUDF::from_impl("insensitive", Vec::<SedonaAccumulatorRef>::new())
+ .with_order_sensitivity(AggregateOrderSensitivity::Insensitive);
+
+ assert_eq!(
+ udf.order_sensitivity(),
+ AggregateOrderSensitivity::Insensitive
+ );
+ }
}
diff --git a/rust/sedona-expr/src/function_set.rs b/rust/sedona-expr/src/function_set.rs
index 3d8620856..eb822c0ae 100644
--- a/rust/sedona-expr/src/function_set.rs
+++ b/rust/sedona-expr/src/function_set.rs
@@ -18,7 +18,7 @@ use crate::{
aggregate_udf::{IntoSedonaAccumulatorRefs, SedonaAggregateUDF},
scalar_udf::{IntoScalarKernelRefs, SedonaScalarUDF},
};
-use datafusion_common::error::Result;
+use datafusion_common::{error::Result, exec_err};
use datafusion_expr::{AggregateUDFImpl, ScalarUDFImpl};
use std::collections::HashMap;
@@ -130,6 +130,27 @@ impl FunctionSet {
Ok(self.aggregate_udf(name).unwrap())
}
+
+ /// Add an aggregate UDF declaration to this set.
+ ///
+ /// If another backend has already declared the same function, its kernels
+ /// are appended after verifying that both declarations agree about order
+ /// sensitivity.
+ pub fn add_aggregate_udf(&mut self, udf: SedonaAggregateUDF) -> Result<&SedonaAggregateUDF> {
+ let name = udf.name().to_string();
+ if let Some(function) = self.aggregate_udf_mut(&name) {
+ if function.order_sensitivity() != udf.order_sensitivity() {
+ return exec_err!(
+ "Conflicting order sensitivity declarations for aggregate function {name}"
+ );
+ }
+ function.add_kernel(udf.kernels().to_vec());
+ } else {
+ self.insert_aggregate_udf(udf);
+ }
+
+ Ok(self.aggregate_udf(&name).unwrap())
+ }
}
impl Default for FunctionSet {
@@ -145,7 +166,9 @@ mod tests {
use arrow_schema::{DataType, FieldRef};
use datafusion_common::{not_impl_err, scalar::ScalarValue};
- use datafusion_expr::{Accumulator, ColumnarValue, Volatility};
+ use datafusion_expr::{
+ Accumulator, ColumnarValue, Volatility, utils::AggregateOrderSensitivity,
+ };
use sedona_schema::{datatypes::SedonaType, matchers::ArgMatcher};
use crate::{
@@ -287,5 +310,21 @@ mod tests {
.into_iter()
.collect::<HashSet<_>>()
);
+
+ let insensitive = SedonaAggregateUDF::from_impl("merged_udaf", kernel.clone())
+ .with_order_sensitivity(AggregateOrderSensitivity::Insensitive);
+ functions.add_aggregate_udf(insensitive).unwrap();
+ let insensitive = SedonaAggregateUDF::from_impl("merged_udaf", kernel.clone())
+ .with_order_sensitivity(AggregateOrderSensitivity::Insensitive);
+ let merged = functions.add_aggregate_udf(insensitive).unwrap();
+ assert_eq!(merged.kernels().len(), 2);
+ assert_eq!(
+ merged.order_sensitivity(),
+ AggregateOrderSensitivity::Insensitive
+ );
+
+ let conflicting = SedonaAggregateUDF::from_impl("merged_udaf", kernel);
+ let err = functions.add_aggregate_udf(conflicting).unwrap_err();
+ assert!(err.message().contains("Conflicting order sensitivity"));
}
}
diff --git a/rust/sedona-functions/src/st_analyze_agg.rs b/rust/sedona-functions/src/st_analyze_agg.rs
index 29d4e22bd..e3c3f9bdc 100644
--- a/rust/sedona-functions/src/st_analyze_agg.rs
+++ b/rust/sedona-functions/src/st_analyze_agg.rs
@@ -30,8 +30,7 @@ use datafusion_common::{
error::{DataFusionError, Result},
exec_datafusion_err,
};
-use datafusion_expr::Volatility;
-use datafusion_expr::{Accumulator, ColumnarValue};
+use datafusion_expr::{Accumulator, ColumnarValue, Volatility, utils::AggregateOrderSensitivity};
use sedona_common::{sedona_internal_datafusion_err, sedona_internal_err};
use sedona_expr::aggregate_udf::{SedonaAccumulatorRef, SedonaAggregateUDF};
use sedona_expr::item_crs::ItemCrsSedonaAccumulator;
@@ -57,6 +56,7 @@ pub fn st_analyze_agg_udf() -> SedonaAggregateUDF {
ItemCrsSedonaAccumulator::wrap_impl(st_analyze_agg_impl()),
Volatility::Immutable,
)
+ .with_order_sensitivity(AggregateOrderSensitivity::Insensitive)
}
/// ST_Analyze_Agg() implementation for geometry types
diff --git a/rust/sedona-functions/src/st_envelope_agg.rs b/rust/sedona-functions/src/st_envelope_agg.rs
index a92d41d58..1b5f9ec25 100644
--- a/rust/sedona-functions/src/st_envelope_agg.rs
+++ b/rust/sedona-functions/src/st_envelope_agg.rs
@@ -26,7 +26,10 @@ use datafusion_common::{
cast::as_float64_array,
error::{DataFusionError, Result},
};
-use datafusion_expr::{Accumulator, ColumnarValue, EmitTo, GroupsAccumulator, Volatility};
+use datafusion_expr::{
+ Accumulator, ColumnarValue, EmitTo, GroupsAccumulator, Volatility,
+ utils::AggregateOrderSensitivity,
+};
use sedona_common::sedona_internal_err;
use sedona_expr::{
aggregate_udf::{SedonaAccumulator, SedonaAggregateUDF},
@@ -58,6 +61,7 @@ pub fn st_envelope_agg_udf() -> SedonaAggregateUDF {
)]),
Volatility::Immutable,
)
+ .with_order_sensitivity(AggregateOrderSensitivity::Insensitive)
}
/// Generic ST_Envelope_Agg accumulator that works with any WkbBounder2D implementation
diff --git a/rust/sedona-geo/benches/geo-functions.rs b/rust/sedona-geo/benches/geo-functions.rs
index 09f14563e..2cf33442f 100644
--- a/rust/sedona-geo/benches/geo-functions.rs
+++ b/rust/sedona-geo/benches/geo-functions.rs
@@ -107,8 +107,8 @@ fn criterion_benchmark(c: &mut Criterion) {
fn criterion_benchmark_aggr(c: &mut Criterion) {
let mut f = sedona_functions::register::default_function_set();
- for (name, kernel) in sedona_geo::register::aggregate_kernels() {
- f.add_aggregate_udf_kernel(name, kernel).unwrap();
+ for udf in sedona_geo::register::aggregate_udfs() {
+ f.add_aggregate_udf(udf).unwrap();
}
// st_intersection_agg would need its own configuration because most of the generated
diff --git a/rust/sedona-geo/src/register.rs b/rust/sedona-geo/src/register.rs
index 70fc7296a..d711e48ed 100644
--- a/rust/sedona-geo/src/register.rs
+++ b/rust/sedona-geo/src/register.rs
@@ -14,7 +14,8 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-use sedona_expr::aggregate_udf::SedonaAccumulatorRef;
+use datafusion_expr::utils::AggregateOrderSensitivity;
+use sedona_expr::aggregate_udf::{SedonaAccumulatorRef, SedonaAggregateUDF};
use sedona_expr::scalar_udf::{IntoScalarKernelRefs, ScalarKernelRef};
macro_rules! define_scalar_kernels {
@@ -59,3 +60,13 @@ pub fn aggregate_kernels() -> Vec<(&'static str, Vec<SedonaAccumulatorRef>)> {
"st_union_agg" => crate::st_union_agg::st_union_agg_impl,
)
}
+
+pub fn aggregate_udfs() -> Vec<SedonaAggregateUDF> {
+ aggregate_kernels()
+ .into_iter()
+ .map(|(name, kernels)| {
+ SedonaAggregateUDF::from_impl(name, kernels)
+ .with_order_sensitivity(AggregateOrderSensitivity::Insensitive)
+ })
+ .collect()
+}
diff --git a/rust/sedona-query-planner/src/optimizer.rs b/rust/sedona-query-planner/src/optimizer.rs
index ee2b9ab7e..5840a9a85 100644
--- a/rust/sedona-query-planner/src/optimizer.rs
+++ b/rust/sedona-query-planner/src/optimizer.rs
@@ -24,15 +24,55 @@ use crate::spatial_expr_utils::{
};
use crate::wrap_async_udf::WrapAsyncUdfRule;
use datafusion::execution::session_state::SessionStateBuilder;
-use datafusion_common::tree_node::Transformed;
-use datafusion_common::{NullEquality, Result};
+use datafusion_common::config::ConfigOptions;
+use datafusion_common::tree_node::{Transformed, TransformedResult};
+use datafusion_common::{NullEquality, Result, plan_err};
use datafusion_expr::logical_plan::Extension;
use datafusion_expr::utils::{conjunction, split_conjunction};
use datafusion_expr::{BinaryExpr, Expr, Operator};
use datafusion_expr::{Filter, Join, JoinType, LogicalPlan};
-use datafusion_optimizer::{ApplyOrder, Optimizer, OptimizerConfig, OptimizerRule};
+use datafusion_optimizer::{AnalyzerRule, ApplyOrder, Optimizer, OptimizerConfig, OptimizerRule};
use sedona_common::option::SedonaOptions;
use sedona_common::{sedona_internal_datafusion_err, sedona_internal_err};
+use sedona_expr::aggregate_udf::SedonaAggregateUDF;
+
+/// Reject ordered Sedona aggregates unless they explicitly declare that input
+/// ordering does not affect their result.
+#[derive(Default, Debug)]
+struct RejectOrderedAggregates;
+
+impl AnalyzerRule for RejectOrderedAggregates {
+ fn analyze(&self, plan: LogicalPlan, _config: &ConfigOptions) -> Result<LogicalPlan> {
+ plan.transform_up_with_subqueries(|plan| plan.map_expressions(reject_ordered_aggregate))
+ .data()
+ }
+
+ fn name(&self) -> &str {
+ "sedona.reject_ordered_aggregates"
+ }
+}
+
+fn reject_ordered_aggregate(expr: Expr) -> Result<Transformed<Expr>> {
+ if let Expr::AggregateFunction(ref aggregate) = expr
+ && let Some(udf) = aggregate.func.inner().downcast_ref::<SedonaAggregateUDF>()
+ && !aggregate.params.order_by.is_empty()
+ && !udf.order_sensitivity().is_insensitive()
+ {
+ return plan_err!(
+ "ORDER BY is not supported for aggregate function {}",
+ aggregate.func.name()
+ );
+ }
+
+ Ok(Transformed::no(expr))
+}
+
+/// Register Sedona-specific semantic validation rules.
+pub fn register_sedona_analyzer_rules(
+ session_state_builder: SessionStateBuilder,
+) -> Result<SessionStateBuilder> {
+ Ok(session_state_builder.with_analyzer_rule(Arc::new(RejectOrderedAggregates)))
+}
/// Replace DataFusion 54.1's leaf projection pushdown rule with the version
/// containing the `Unnest` barrier added by DataFusion PR #22620.
diff --git a/rust/sedona/src/context.rs b/rust/sedona/src/context.rs
index fac6c3a47..a614f2592 100644
--- a/rust/sedona/src/context.rs
+++ b/rust/sedona/src/context.rs
@@ -74,8 +74,8 @@ use sedona_spatial_join_gpu::options::GpuOptions;
use sedona_query_planner::{
optimizer::{
- register_ensure_loaded_optimizer, register_spatial_join_logical_optimizer,
- register_vendored_optimizer_rules,
+ register_ensure_loaded_optimizer, register_sedona_analyzer_rules,
+ register_spatial_join_logical_optimizer, register_vendored_optimizer_rules,
},
query_planner::SedonaQueryPlanner,
raster_batch_budget::RasterBatchBudgetRule,
@@ -108,6 +108,7 @@ impl SedonaContext {
// DataFusion #22620 workaround tracked by
// https://github.com/apache/sedona-db/issues/1232.
let state_builder = register_vendored_optimizer_rules(state_builder).unwrap();
+ let state_builder = register_sedona_analyzer_rules(state_builder).unwrap();
Self::finish_new(SessionContext::new_with_state(state_builder.build())).unwrap()
}
@@ -259,6 +260,7 @@ impl SedonaContext {
// DataFusion #22620 workaround tracked by
// https://github.com/apache/sedona-db/issues/1232.
state_builder = register_vendored_optimizer_rules(state_builder)?;
+ state_builder = register_sedona_analyzer_rules(state_builder)?;
state_builder = register_spatial_join_logical_optimizer(state_builder)?;
state_builder = register_ensure_loaded_optimizer(state_builder)?;
// Re-batch raster materialization by estimated bytes rather than
@@ -404,9 +406,9 @@ impl SedonaContext {
#[cfg(feature = "geos")]
out.register_scalar_kernels(sedona_geos::register::scalar_kernels().into_iter())?;
- // Register geos aggregate kernels if built with geos support
+ // Register geos aggregate UDFs if built with geos support
#[cfg(feature = "geos")]
- out.register_aggregate_kernels(sedona_geos::register::aggregate_kernels().into_iter())?;
+ out.register_aggregate_udfs(sedona_geos::register::aggregate_udfs().into_iter())?;
// Register geo kernels if built with geo support
#[cfg(feature = "geo")]
@@ -415,9 +417,9 @@ impl SedonaContext {
#[cfg(feature = "tg")]
out.register_scalar_kernels(sedona_tg::register::scalar_kernels().into_iter())?;
- // Register geo aggregate kernels if built with geo support
+ // Register geo aggregate UDFs if built with geo support
#[cfg(feature = "geo")]
- out.register_aggregate_kernels(sedona_geo::register::aggregate_kernels().into_iter())?;
+ out.register_aggregate_udfs(sedona_geo::register::aggregate_udfs().into_iter())?;
// Register s2geography scalar kernels if built with s2geography support
#[cfg(feature = "s2geography")]
@@ -439,9 +441,7 @@ impl SedonaContext {
sd_order_lnglat::OrderLngLat::new(sedona_s2geography::utils::s2_cell_id_from_lnglat);
self.register_scalar_kernels([("sd_order", sd_order_kernel)].into_iter())?;
- self.register_aggregate_kernels(
- sedona_s2geography::register::aggregate_kernels().into_iter(),
- )?;
+ self.register_aggregate_udfs(sedona_s2geography::register::aggregate_udfs().into_iter())?;
Ok(())
}
@@ -584,6 +584,19 @@ impl SedonaContext {
Ok(())
}
+ pub fn register_aggregate_udfs(
+ &mut self,
+ udfs: impl Iterator<Item = SedonaAggregateUDF>,
+ ) -> Result<()> {
+ let mut functions = self.functions_mut()?;
+ for udf in udfs {
+ let udf = functions.add_aggregate_udf(udf)?;
+ self.ctx.register_udaf(udf.clone().into());
+ }
+
+ Ok(())
+ }
+
/// Creates a [`DataFrame`] from SQL query text that may contain one or more
/// statements
pub async fn multi_sql(&self, sql: &str) -> Result<Vec<DataFrame>> {
@@ -1077,6 +1090,67 @@ mod tests {
);
}
+ #[tokio::test]
+ async fn ordered_sedona_aggregates_respect_order_sensitivity() {
+ let ctx = SedonaContext::new();
+
+ let values = "FROM (VALUES (2, ST_Point(2, 2)), (1, ST_Point(1, 1))) AS t(x, g)";
+
+ let sql = format!("SELECT ST_Collect_Agg(g ORDER BY x) {values}");
+ let err = ctx.sql(&sql).await.unwrap().collect().await.unwrap_err();
+ assert!(
+ err.to_string()
+ .contains("ORDER BY is not supported for aggregate function st_collect_agg"),
+ "unexpected error: {err}"
+ );
+
+ for function in [
+ "ST_Analyze_Agg",
+ "ST_Envelope_Agg",
+ "ST_ConvexHull_Agg",
+ "ST_Intersection_Agg",
+ "ST_Union_Agg",
+ "ST_Polygonize_Agg",
+ ] {
+ let sql = format!("SELECT {function}(g ORDER BY x) {values}");
+ ctx.sql(&sql)
+ .await
+ .unwrap()
+ .collect()
+ .await
+ .unwrap_or_else(|err| panic!("{function} unexpectedly failed: {err}"));
+ }
+
+ // DataFusion's own ordered aggregates remain available.
+ ctx.sql("SELECT array_agg(x ORDER BY x) FROM (VALUES (2), (1)) AS t(x)")
+ .await
+ .unwrap()
+ .collect()
+ .await
+ .unwrap();
+ }
+
+ #[tokio::test]
+ async fn ordered_sedona_aggregates_in_subqueries_are_rejected() {
+ let ctx = SedonaContext::new();
+
+ let sql = r#"
+ SELECT ST_AsText((
+ SELECT ST_Collect_Agg(g ORDER BY x)
+ FROM (VALUES
+ (2, ST_Point(2, 2)),
+ (1, ST_Point(1, 1))
+ ) AS t(x, g)
+ ))
+ "#;
+ let err = ctx.sql(sql).await.unwrap().collect().await.unwrap_err();
+ assert!(
+ err.to_string()
+ .contains("ORDER BY is not supported for aggregate function st_collect_agg"),
+ "unexpected error: {err}"
+ );
+ }
+
#[cfg(feature = "s2geography")]
#[tokio::test]
async fn geography_bounds_default_context() {