use super::*;
use crate::plan::{RelationalPlan, UnifiedPlan};
use crate::routines::RoutineResolution;
struct Catalog;
impl FunctionTypeResolver for Catalog {
fn resolve_type_name(&self, name: &str) -> Result<Option<ColumnType>, SQLError> {
if let Some(element) = name.strip_suffix("[]") {
return self
.resolve_type_name(element)
.map(|element| element.map(|element| ColumnType::Array(Box::new(element))));
}
let name = name.replace('"', "");
if name == "grouping_composite" {
return Ok(Some(ColumnType::Composite(
crate::ast::CompositeTypeReference {
schema: "public".into(),
name,
oid: 50_002,
array_oid: 50_003,
relation_oid: 50_004,
},
)));
}
if name == "grouping_enum" {
return Ok(Some(ColumnType::Enum(crate::ast::EnumTypeReference {
schema: "public".into(),
name,
oid: 50_005,
array_oid: 50_006,
})));
}
Ok(matches!(
name.as_str(),
"grouping_literal_numeric" | "public.grouping_literal_numeric"
)
.then(|| ColumnType::Domain {
collation_oid: None,
schema: "public".into(),
name: "grouping_literal_numeric".into(),
oid: 50_001,
array_oid: None,
base: Box::new(ColumnType::Numeric {
precision: None,
scale: None,
}),
}))
}
fn resolve_function_type(
&self,
_name: &str,
_binding: Option<&crate::ast::FunctionBinding>,
_argument_names: &[Option<String>],
_argument_types: &[Option<ColumnType>],
_explicit_variadic: bool,
) -> Result<Option<ColumnType>, SQLError> {
Ok(None)
}
}
impl RoutineResolution for Catalog {}
impl crate::plan::AggregateClassifier for Catalog {
fn is_registered_aggregate(&self, _name: &str) -> bool {
false
}
}
fn block(sql: &str) -> QueryBlockPlan {
let UnifiedPlan::Query(query) = UnifiedPlan::lower(crate::compile(sql).unwrap().remove(0))
else {
panic!("query")
};
let RelationalPlan::QueryBlock(block) = query.root else {
panic!("query block")
};
*block
}
fn input() -> RowSchema {
RowSchema::with_types(
vec!["id".into(), "n".into()],
vec![Some(ColumnType::Integer); 2],
)
}
#[test]
fn grouped_literal_validation_matches_postgresql_before_evaluation() {
let fixture: serde_json::Value =
serde_json::from_str(include_str!("../aggregates/pg18_literals.json")).unwrap();
let schema = RowSchema::with_identities(
vec!["n".into()],
vec![crate::ColumnIdentity::qualified("t", "n")],
vec![Some(ColumnType::Integer)],
);
let mut differences = Vec::new();
for case in fixture["cases"].as_array().unwrap() {
let sql = case["sql"].as_str().unwrap();
if sql.starts_with("PREPARE ") {
continue;
}
let result = validate_grouped_expressions(&Catalog, &block(sql), &schema, None, &[]);
match (case["sqlstate"].as_str(), result) {
(None, Ok(())) => {}
(Some(expected), Err(error))
if error.sqlstate() == Some(expected)
&& error.to_string() == case["message"].as_str().unwrap() => {}
(expected, actual) => differences.push(format!(
"{}: expected {expected:?}, got {actual:?}",
case["name"]
)),
}
}
assert!(differences.is_empty(), "{}", differences.join("\n"));
}
#[test]
fn grouping_input_names_precede_aggregate_and_scalar_output_aliases() {
for sql in [
"SELECT min(id) AS first_id, count(*) AS n FROM t GROUP BY n",
"SELECT min(id) AS first_id, 0 AS n FROM t GROUP BY n",
] {
let original = block(sql);
assert!(prepare_grouping_sets(&Catalog, &original, &input(), &[])
.unwrap()
.is_none());
assert!(matches!(original.group_by.as_slice(), [ScalarExpr::Column(name)] if name == "n"));
}
}
#[test]
fn grouping_aliases_resolve_before_distinct_sets_and_storage() {
let original = block("SELECT n + 1 AS shifted, count(*) FROM t GROUP BY DISTINCT GROUPING SETS ((shifted), (n + 1))");
let prepared = prepare_grouping_sets(&Catalog, &original, &input(), &[])
.unwrap()
.unwrap();
assert_eq!(prepared.grouping_sets.len(), 1);
assert_eq!(
prepared.grouping_sets[0],
[original.projections[0].expr.clone()]
);
let mut stored = original;
assert!(bind_grouping_names(&Catalog, &mut stored, &input(), None, &[]).unwrap());
assert!(!bind_grouping_names(&Catalog, &mut stored, &input(), None, &[]).unwrap());
assert_eq!(stored.grouping_sets[0], stored.grouping_sets[1]);
}
#[test]
fn grouping_set_computed_keys_remain_whole_aggregate_dependencies() {
let original = block("SELECT n + 1 AS shifted, count(*) FROM t GROUP BY DISTINCT GROUPING SETS ((shifted), (n + 1), ())");
let prepared = prepare_grouping_sets(&Catalog, &original, &input(), &[])
.unwrap()
.unwrap();
let output = crate::semantics::sets::rewrite::prepare_aggregate_output_projection(
&Catalog,
&prepared,
&[],
);
assert_eq!(
output.statement.projections[0].expr,
original.projections[0].expr
);
assert!(matches!(output.projections[0].1, ScalarExpr::Position(0)));
assert_eq!(output.statement.grouping_sets.len(), 2);
}
#[test]
fn grouping_duplicate_aliases_use_analyzed_expression_identity() {
let identical = block("SELECT n AS x, n AS x FROM t GROUP BY x");
assert!(prepare_grouping_sets(&Catalog, &identical, &input(), &[])
.unwrap()
.is_some());
for sql in [
"SELECT id AS x, n AS x FROM t GROUP BY x",
"SELECT 1 AS x, 1.0 AS x GROUP BY x",
] {
assert_eq!(
prepare_grouping_sets(&Catalog, &block(sql), &input(), &[])
.unwrap_err()
.sqlstate(),
Some("42702"),
"{sql}"
);
}
}
#[test]
fn grouping_identity_ignores_common_cast_display_origin() {
let schema = RowSchema::with_types(
vec!["s".into(), "b".into()],
vec![Some(ColumnType::SmallInteger), Some(ColumnType::BigInteger)],
);
let original = block("SELECT coalesce(s,b) AS grouped_value, count(*) FROM t GROUP BY DISTINCT GROUPING SETS ((coalesce(s,b)), (coalesce(s::bigint,b)))");
let prepared = prepare_grouping_sets(&Catalog, &original, &schema, &[])
.unwrap()
.unwrap();
assert_eq!(prepared.grouping_sets.len(), 1);
let aliases = block("SELECT coalesce(s,b) AS grouped_value, coalesce(s::bigint,b) AS grouped_value FROM t GROUP BY grouped_value");
assert!(prepare_grouping_sets(&Catalog, &aliases, &schema, &[])
.unwrap()
.is_some());
}
#[test]
fn grouping_aliases_retain_aggregate_and_window_context_errors() {
for (sql, state) in [
("SELECT count(*) AS tally FROM t GROUP BY tally", "42803"),
(
"SELECT row_number() OVER () AS rank FROM t GROUP BY rank",
"42P20",
),
] {
assert_eq!(
prepare_grouping_sets(&Catalog, &block(sql), &input(), &[])
.unwrap_err()
.sqlstate(),
Some(state),
"{sql}"
);
}
}
#[test]
fn grouping_names_inside_expressions_are_never_output_aliases() {
let original = block("SELECT n + 1 AS shifted FROM t GROUP BY shifted + 1");
assert!(prepare_grouping_sets(&Catalog, &original, &input(), &[])
.unwrap()
.is_none());
}
#[test]
fn grouping_identity_keeps_catalog_casts_for_the_catalog_resolver() {
for ty in [
"regclass",
"regnamespace",
"regrole",
"regtype",
"regproc",
"regprocedure",
"grouping_literal_numeric",
"grouping_literal_numeric[]",
"grouping_composite",
"grouping_composite[]",
"grouping_enum",
"grouping_enum[]",
"regtype[]",
] {
let input = ScalarExpr::Literal(Value::Str("catalog-dependent input".into()));
let expression = ScalarExpr::Cast {
implicit: false,
expr: Box::new(input.clone()),
ty: ty.into(),
};
let normalized = normalize_expression(
&Catalog,
expression.clone(),
&RowSchema::with_types(Vec::new(), Vec::new()),
&[],
)
.unwrap();
let ScalarExpr::Cast { expr, .. } = normalized else {
panic!("{ty}: catalog input was evaluated during grouping analysis");
};
assert_eq!(*expr, input, "{ty}");
}
}
#[test]
fn grouping_distinct_keeps_selected_null_argument_identity() {
let schema = RowSchema::with_qualified_types(
"sales",
vec!["region".into()],
vec![Some(ColumnType::Text)],
);
let crate::Statement::CreateTable(table) = crate::compile("CREATE TABLE sales (region text)")
.unwrap()
.remove(0)
else {
panic!("table")
};
let context = crate::binding::BindingContext {
catalog: crate::binding::fixture::catalog(std::collections::BTreeMap::from([(
crate::RelationIdentity::new("public", "sales"),
crate::binding::fixture::table_definition(table.columns),
)])),
..crate::binding::fixture::empty_binding_context()
};
for (sets, expected) in [
(
"((lower(NULL)),(lower(NULL::text)),(region || NULL),(region || NULL::text))",
2,
),
(
r#"((lower(NULL)),(lower(NULL::varchar)),(lower(NULL::text COLLATE "C")),(lower(NULL::text COLLATE "default")))"#,
4,
),
(
r#"((lower('A'::text COLLATE "C")),(lower('A'::text COLLATE "POSIX")))"#,
2,
),
] {
let sql = format!("SELECT count(*) FROM sales GROUP BY DISTINCT GROUPING SETS {sets}");
for retained in [false, true] {
let mut plan = UnifiedPlan::lower(crate::compile(&sql).unwrap().remove(0));
if retained {
crate::binding::read_prepared_inputs(&Catalog, &mut plan, &[], &context, None)
.unwrap();
}
let UnifiedPlan::Query(query) = plan else {
panic!("query")
};
let RelationalPlan::QueryBlock(original) = query.root else {
panic!("block")
};
let prepared = prepare_grouping_sets(&Catalog, &original, &schema, &[])
.unwrap()
.unwrap();
assert_eq!(
prepared.grouping_sets.len(),
expected,
"retained={retained}: {sql}"
);
}
}
}
#[test]
fn grouping_identity_preserves_collation_selection_state() {
use crate::catalog::collations::derivation::Derivation;
let schema = RowSchema::default();
let literal = ScalarExpr::TypedLiteral {
value: Value::Null,
ty: ColumnType::Text.catalog_name(),
bound_type: Some(ColumnType::Text),
parameter_index: None,
collation_oid: None,
composite_source: None,
};
let normalize = |expression| normalize_expression(&Catalog, expression, &schema, &[]).unwrap();
let selected = |derivation, initial_oid| ScalarExpr::Collated {
expr: Box::new(literal.clone()),
ty: ColumnType::Text,
collation: crate::ir::ResultCollation::new(derivation, initial_oid),
};
assert_eq!(
normalize(selected(Derivation::Implicit(100), 100)),
normalize(literal.clone())
);
let selections = [
selected(Derivation::Implicit(950), 950),
selected(Derivation::Implicit(951), 951),
selected(Derivation::Explicit(100), 100),
selected(Derivation::Conflict(950, 951), 0),
selected(Derivation::Implicit(100), 0),
];
let identities: Vec<_> = selections.into_iter().map(normalize).collect();
for (index, identity) in identities.iter().enumerate() {
assert_ne!(*identity, normalize(literal.clone()));
assert_eq!(*identity, normalize(identity.clone()));
assert!(identities[..index].iter().all(|other| other != identity));
}
}