Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
as initializing a comment (#555).
- Fixed a bug in stat_aggregate prevented transposed layers from properly
aggregating in certain situations (#561)
- Fixed schema-less `validate()` rejecting spatial layers that omit an explicit
`geometry` mapping, even though execution auto-detects the geometry column
(#587)

## 0.5.2 - 2026-09-11

Expand Down
71 changes: 71 additions & 0 deletions src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,19 @@ pub fn validate(query: &str) -> Result<Validated> {
.or_insert(value.clone());
}
}
// Geoms with a `geometry` aesthetic (e.g. spatial) auto-detect
// the geometry column from the result schema at execution time
// (see detect_geometry_column in execute/mod.rs), so don't
// require an explicit mapping in schema-less validation.
if merged.geom.aesthetics().is_required("geometry") {
merged
.mappings
.aesthetics
.entry("geometry".to_string())
.or_insert(crate::plot::AestheticValue::standard_column(
"geometry".to_string(),
));
}
if let Err(e) = merged.validate_mapping(&plot.aesthetic_context, false) {
errors.push(ValidationError {
message: format!("{}: {}", context, e),
Expand Down Expand Up @@ -493,4 +506,62 @@ mod tests {
);
assert!(!validated.has_visual());
}

#[test]
#[cfg(feature = "spatial")]
fn test_validate_spatial_geometry_mapping_optional() {
// The spatial geom auto-detects its geometry column from the result
// schema at execution time, so schema-less validation must not
// require an explicit `geometry` mapping.
let query = "SELECT name, geom, gdp FROM world VISUALISE DRAW spatial MAPPING gdp AS fill";
let validated = validate(query).unwrap();
assert!(
validated.valid(),
"spatial layer without explicit geometry mapping should be valid: {:?}",
validated.errors()
);

// An explicit geometry mapping remains valid too.
let query = "SELECT name, geom, gdp FROM world VISUALISE DRAW spatial MAPPING geom AS geometry, gdp AS fill";
let validated = validate(query).unwrap();
assert!(
validated.valid(),
"spatial layer with explicit geometry mapping should be valid: {:?}",
validated.errors()
);
}

#[test]
#[cfg(feature = "spatial")]
fn test_validate_spatial_map_patterns() {
// The canonical map patterns: all rely on geometry auto-detection and
// must pass schema-less validation.

// Choropleth: user data joined to ggsql:world, fill mapping only.
let choropleth = "SELECT w.name, w.geom, s.revenue FROM ggsql:world w JOIN sales_by_country s ON w.iso_a3 = s.country_code VISUALISE DRAW spatial MAPPING revenue AS fill";
let validated = validate(choropleth).unwrap();
assert!(
validated.valid(),
"choropleth should be valid: {:?}",
validated.errors()
);

// Point map: null-sentinel fill, layer-level FROM, named projection.
let point_map = "SELECT city, lon, lat, passengers FROM airports VISUALISE DRAW spatial MAPPING null AS fill FROM ggsql:world DRAW point MAPPING lon, lat, passengers AS size PROJECT TO natural";
let validated = validate(point_map).unwrap();
assert!(
validated.valid(),
"point map should be valid: {:?}",
validated.errors()
);

// Orthographic globe with an explicit origin.
let globe = "SELECT longitude, latitude, mag FROM quakes VISUALISE DRAW spatial MAPPING null AS fill FROM ggsql:world DRAW point MAPPING longitude AS lon, latitude AS lat, mag AS size PROJECT TO orthographic SETTING origin => (-165, 25)";
let validated = validate(globe).unwrap();
assert!(
validated.valid(),
"orthographic globe should be valid: {:?}",
validated.errors()
);
}
}
Loading