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() {