From 8219f7d2bd2f8c5268049ecd94173b559876eb60 Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:12:31 +0700 Subject: [PATCH] Read a string literal in an aggregate call as PostgreSQL resolves it --- crates/rudb-bind/src/binder.rs | 3 +++ crates/rudb-kernels/src/pgregression.rs | 6 ++++- crates/rudb-server/tests/server.rs | 33 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/rudb-bind/src/binder.rs b/crates/rudb-bind/src/binder.rs index 994484e6..2d2e3364 100644 --- a/crates/rudb-bind/src/binder.rs +++ b/crates/rudb-bind/src/binder.rs @@ -7062,6 +7062,9 @@ impl<'a> Binder<'a> { if parts.frame.unit == WindowUnit::Range && offsets && parts.order.len() != 1 { return Err(Error::binder("RANGE frames must have only one ORDER BY expression")); } + if self.semantics.unknown_types() == UnknownTypes::Postgres { + self.pg_aggregate_unknowns(ast, name, args, &mut parts.args)?; + } let types: Vec = parts.args.iter().map(|&arg| self.plan.expr_type(arg).clone()).collect(); diff --git a/crates/rudb-kernels/src/pgregression.rs b/crates/rudb-kernels/src/pgregression.rs index 569f5de4..8b8a2f02 100644 --- a/crates/rudb-kernels/src/pgregression.rs +++ b/crates/rudb-kernels/src/pgregression.rs @@ -249,7 +249,8 @@ fn r2(sxx: f64, syy: f64, sxy: f64) -> f64 { } false => numerator / denominator, }; - answer.min(1.0) + // A NaN stays a NaN, where `f64::min` would give the one. + if answer > 1.0 { 1.0 } else { answer } } /// The `Sx * Sxy / Sxx` that `regr_intercept` takes from the sum of the `y` values, through the @@ -344,6 +345,9 @@ mod tests { state.add(1.0, 1e308); state.add(1.0, 1e308); assert!(state.finish().is_err()); + let nan: Vec<(f64, f64)> = (1..=3).map(|g| (f64::from(g), f64::NAN)).collect(); + let Value::Double(r2) = answer("float8_regr_r2", &nan) else { panic!("a double") }; + assert!(r2.is_nan()); let mut infinite = FloatPairs::named("float8_corr").unwrap(); infinite.add(1.0, f64::INFINITY); infinite.add(2.0, 3.0); diff --git a/crates/rudb-server/tests/server.rs b/crates/rudb-server/tests/server.rs index 46d8117d..7e1a399b 100644 --- a/crates/rudb-server/tests/server.rs +++ b/crates/rudb-server/tests/server.rs @@ -7622,6 +7622,39 @@ fn a_limit_in_a_correlated_subquery_reads_the_outer_row_as_postgres() { server.stop().unwrap(); } +#[test] +fn an_aggregate_reads_a_string_literal_as_postgres() { + let dirs = Dirs::new("pgaggunknown"); + let server = Server::start(dirs.config()).unwrap(); + let mut client = Client::unix(&server); + connect(&mut client, PROTOCOL_3_0); + // The answers are the ones that PostgreSQL 19 gives. A string literal takes the type of the + // aggregate that PostgreSQL finds for it, over a group and over a window. + for (sql, expected) in [ + ( + "select concat_ws('|', corr(g, 'NaN'), regr_r2(g, 'NaN'), min('a'), \ + string_agg('x', ',')) from generate_series(1, 3) g", + "NaN|NaN|a|x,x,x", + ), + ( + "select string_agg(coalesce(c::text, 'null'), ' ') from (select corr(g, '2') \ + over (order by g) c from generate_series(1, 3) g) q", + "null null null", + ), + ] { + assert_eq!(scalar(&mut client, sql), expected, "{sql}"); + } + for sql in ["select avg('4') from generate_series(1, 2)", "select sum('x') over ()"] { + let messages = client.query(sql); + let tags = tags(&messages); + assert!(tags.ends_with("EZ"), "{tags}"); + let error = &messages[tags.len() - 2]; + assert_eq!(error.field(b'C').as_deref(), Some("42725"), "{sql}"); + assert_eq!(error.field(b'D').as_deref(), Some("Could not choose a best candidate function.")); + } + server.stop().unwrap(); +} + #[test] fn a_pair_statistic_of_a_constant_column_is_null_as_postgres() { let dirs = Dirs::new("pgpairs");