Skip to content
14 changes: 6 additions & 8 deletions src/execute/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! This module handles building SQL queries for layers, applying pre-stat
//! transformations, stat transforms, and post-query operations.

use crate::plot::aesthetic::{self, AestheticContext};
use crate::plot::aesthetic;
use crate::plot::layer::is_transposed;
use crate::plot::layer::orientation::{flip_position_aesthetics, resolve_orientation};
use crate::plot::{
Expand Down Expand Up @@ -276,7 +276,6 @@ pub fn apply_pre_stat_transform(
aesthetic_schema: &Schema,
scales: &[Scale],
dialect: &dyn SqlDialect,
aesthetic_ctx: &AestheticContext,
) -> String {
let mut transform_exprs: Vec<(String, String)> = vec![];
let mut transformed_columns: HashSet<String> = HashSet::new();
Expand All @@ -290,7 +289,6 @@ pub fn apply_pre_stat_transform(
&layer.parameters,
&layer.mappings,
aesthetic_schema,
aesthetic_ctx,
layer.geom.aggregate_domain_aesthetics().unwrap_or(&[]),
);

Expand Down Expand Up @@ -476,7 +474,6 @@ pub fn apply_layer_transforms<F>(
scales: &[Scale],
dialect: &dyn SqlDialect,
execute_query: &F,
aesthetic_ctx: &AestheticContext,
) -> Result<String>
where
F: Fn(&str) -> Result<DataFrame>,
Expand Down Expand Up @@ -523,7 +520,6 @@ where
&aesthetic_schema,
scales,
dialect,
aesthetic_ctx,
);

// Build group_by columns from partition_by
Expand Down Expand Up @@ -553,7 +549,6 @@ where
&layer.parameters,
execute_query,
dialect,
aesthetic_ctx,
)?;

// Flip user remappings BEFORE merging defaults for Transposed orientation.
Expand Down Expand Up @@ -964,7 +959,6 @@ pub fn resolve_orientations(
layers: &mut [Layer],
scales: &[Scale],
layer_type_info: &mut [Vec<super::schema::TypeInfo>],
aesthetic_ctx: &AestheticContext,
) {
for (layer_idx, layer) in layers.iter_mut().enumerate() {
let orientation = resolve_orientation(layer, scales);
Expand All @@ -979,7 +973,11 @@ pub fn resolve_orientations(
if layer_idx < layer_type_info.len() {
for (name, _, _) in &mut layer_type_info[layer_idx] {
if let Some(aesthetic) = naming::extract_aesthetic_name(name) {
let flipped = aesthetic_ctx.flip_position(aesthetic);
let flipped = layer
.mappings
.context()
.map(|ctx| ctx.flip_position(aesthetic))
.unwrap_or_else(|| aesthetic.to_string());
if flipped != aesthetic {
*name = naming::aesthetic_column(&flipped);
}
Expand Down
84 changes: 30 additions & 54 deletions src/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub use schema::TypeInfo;

use crate::naming;
use crate::parser;
use crate::plot::aesthetic::{is_position_aesthetic, AestheticContext};
use crate::plot::aesthetic::is_position_aesthetic;
use crate::plot::facet::{resolve_properties as resolve_facet_properties, FacetDataContext};
use crate::plot::layer::is_transposed;
use crate::plot::projection::resolve_projection_properties;
Expand All @@ -50,25 +50,14 @@ use crate::reader::DuckDBReader;
/// - Partition_by columns exist in schema
/// - Remapping target aesthetics are supported by geom
/// - Remapping source columns are valid stat columns for geom
fn validate(
layers: &[Layer],
layer_schemas: &[Schema],
aesthetic_context: &Option<AestheticContext>,
) -> Result<()> {
let translate = |aes: &str| -> String {
match aesthetic_context {
Some(ctx) => ctx.map_internal_to_user(aes),
None => aes.to_string(),
}
};

fn validate(layers: &[Layer], layer_schemas: &[Schema]) -> Result<()> {
for (idx, (layer, schema)) in layers.iter().zip(layer_schemas.iter()).enumerate() {
let schema_columns: HashSet<&str> = schema.iter().map(|c| c.name.as_str()).collect();
let supported = layer.geom.aesthetics().supported();

// Validate required aesthetics for this geom
layer
.validate_mapping(aesthetic_context, false)
.validate_mapping(false)
.map_err(|e| GgsqlError::ValidationError(format!("Layer {}: {}", idx + 1, e)))?;

// Validate SETTING parameters are valid for this geom
Expand All @@ -92,7 +81,7 @@ fn validate(
return Err(GgsqlError::ValidationError(format!(
"Layer {}: aesthetic '{}' references non-existent column '{}'",
idx + 1,
translate(aesthetic),
layer.mappings.display_name(aesthetic),
col_name
)));
}
Expand All @@ -118,7 +107,7 @@ fn validate(
return Err(GgsqlError::ValidationError(format!(
"Layer {}: REMAPPING targets unsupported aesthetic '{}' for geom '{}'",
idx + 1,
translate(target_aesthetic),
layer.mappings.display_name(target_aesthetic),
layer.geom
)));
}
Expand Down Expand Up @@ -839,7 +828,6 @@ fn add_discrete_columns_to_partition_by(
layers: &mut [Layer],
layer_schemas: &[Schema],
scales: &[Scale],
aesthetic_ctx: &AestheticContext,
) {
// Build a map of aesthetic -> scale for quick lookup
let scale_map: HashMap<&str, &Scale> =
Expand Down Expand Up @@ -875,7 +863,6 @@ fn add_discrete_columns_to_partition_by(
&layer.parameters,
&layer.mappings,
schema,
aesthetic_ctx,
layer.geom.aggregate_domain_aesthetics().unwrap_or(&[]),
)
.map(|(t, _)| t)
Expand Down Expand Up @@ -906,8 +893,10 @@ fn add_discrete_columns_to_partition_by(
//
// Discrete and Binned scales produce categorical groupings.
// Continuous scales don't group. Identity defers to column type.
let primary_aes = aesthetic_ctx
.primary_internal_position(aesthetic)
let primary_aes = layer
.mappings
.context()
.and_then(|ctx| ctx.primary_internal_position(aesthetic))
.unwrap_or(aesthetic);
let is_discrete = if let Some(scale) = scale_map.get(primary_aes) {
if let Some(ref scale_type) = scale.scale_type {
Expand Down Expand Up @@ -1252,11 +1241,7 @@ pub fn prepare_data_with_reader(query: &str, reader: &dyn Reader) -> Result<Prep

// Validate all layers against their schemas
// This must happen BEFORE build_layer_query because stat transforms remove consumed aesthetics
validate(
&specs[0].layers,
&layer_schemas,
&specs[0].aesthetic_context,
)?;
validate(&specs[0].layers, &layer_schemas)?;

// Allow geoms to adjust mappings based on their specific logic
// (e.g., rule geom converts pos1/pos2 to AnnotationColumn when slope is present)
Expand Down Expand Up @@ -1289,13 +1274,7 @@ pub fn prepare_data_with_reader(query: &str, reader: &dyn Reader) -> Result<Prep
// Detect orientation and flip mappings BEFORE building base queries.
// This ensures the SQL query uses the correct aesthetic column names.
let scales = specs[0].scales.clone();
let aesthetic_ctx = specs[0].get_aesthetic_context();
layer::resolve_orientations(
&mut specs[0].layers,
&scales,
&mut layer_type_info,
&aesthetic_ctx,
);
layer::resolve_orientations(&mut specs[0].layers, &scales, &mut layer_type_info);

// Build layer base queries using build_layer_base_query()
// These include: SELECT with aesthetic renames, casts from type_requirements, filters
Expand Down Expand Up @@ -1327,13 +1306,7 @@ pub fn prepare_data_with_reader(query: &str, reader: &dyn Reader) -> Result<Prep

// Add discrete mapped columns to partition_by for all layers
let scales = specs[0].scales.clone();
let aesthetic_ctx = specs[0].get_aesthetic_context();
add_discrete_columns_to_partition_by(
&mut specs[0].layers,
&layer_schemas,
&scales,
&aesthetic_ctx,
);
add_discrete_columns_to_partition_by(&mut specs[0].layers, &layer_schemas, &scales);

// Clone scales for apply_layer_transforms
let scales = specs[0].scales.clone();
Expand Down Expand Up @@ -1363,7 +1336,6 @@ pub fn prepare_data_with_reader(query: &str, reader: &dyn Reader) -> Result<Prep
&scales,
dialect,
&execute_query,
&aesthetic_ctx,
)?;
layer_queries.push(layer_query);
}
Expand Down Expand Up @@ -1461,7 +1433,7 @@ pub fn prepare_data_with_reader(query: &str, reader: &dyn Reader) -> Result<Prep
let flipped_df =
crate::plot::layer::orientation::flip_dataframe_position_columns(
df,
&aesthetic_ctx,
&layer.mappings,
);
data_map.insert(key.clone(), flipped_df);
}
Expand Down Expand Up @@ -3393,9 +3365,10 @@ mod tests {

mod validate_translation_tests {
use super::*;
use crate::plot::aesthetic::AestheticContext;
use crate::plot::layer::geom::Geom;
use crate::plot::layer::Layer;
use crate::plot::types::{AestheticValue, ColumnInfo};
use crate::plot::types::{AestheticValue, ColumnInfo, Mappings};
use arrow::datatypes::DataType;

fn col(name: &str) -> ColumnInfo {
Expand All @@ -3410,6 +3383,7 @@ mod tests {

fn point_layer_with_mapping(aesthetic: &str, column: &str) -> Layer {
let mut layer = Layer::new(Geom::point());
layer.mappings = Mappings::new().with_cartesian_context();
// Point geom requires both pos1 and pos2; map both, but only the
// one we're testing points at a missing column.
layer.mappings.aesthetics.insert(
Expand All @@ -3430,6 +3404,7 @@ mod tests {
#[test]
fn aesthetic_column_missing_translates_pos1_to_x_under_cartesian() {
let mut layer = Layer::new(Geom::point());
layer.mappings = Mappings::new().with_cartesian_context();
layer.mappings.aesthetics.insert(
"pos1".to_string(),
AestheticValue::standard_column("missing"),
Expand All @@ -3439,9 +3414,8 @@ mod tests {
AestheticValue::standard_column("present_y"),
);
let schema: Schema = vec![col("present_y")];
let ctx = Some(AestheticContext::from_static(&["x", "y"], &[]));

let err = validate(&[layer], &[schema], &ctx).unwrap_err().to_string();
let err = validate(&[layer], &[schema]).unwrap_err().to_string();
assert_eq!(
err,
"Validation error: Layer 1: aesthetic 'x' references non-existent column 'missing'"
Expand All @@ -3451,6 +3425,9 @@ mod tests {
#[test]
fn aesthetic_column_missing_translates_pos1_to_angle_under_polar() {
let mut layer = Layer::new(Geom::point());
layer
.mappings
.set_context(AestheticContext::from_static(&["angle", "radius"], &[]));
layer.mappings.aesthetics.insert(
"pos1".to_string(),
AestheticValue::standard_column("missing"),
Expand All @@ -3460,9 +3437,8 @@ mod tests {
AestheticValue::standard_column("present_radius"),
);
let schema: Schema = vec![col("present_radius")];
let ctx = Some(AestheticContext::from_static(&["angle", "radius"], &[]));

let err = validate(&[layer], &[schema], &ctx).unwrap_err().to_string();
let err = validate(&[layer], &[schema]).unwrap_err().to_string();
assert_eq!(
err,
"Validation error: Layer 1: aesthetic 'angle' references non-existent column 'missing'"
Expand All @@ -3473,10 +3449,9 @@ mod tests {
fn aesthetic_column_missing_translates_pos2_to_y_under_cartesian() {
let layer = point_layer_with_mapping("pos2", "missing");
let schema: Schema = vec![col("present_x"), col("present_y")];
let ctx = Some(AestheticContext::from_static(&["x", "y"], &[]));

// pos2 is overridden to point at "missing" by point_layer_with_mapping
let err = validate(&[layer], &[schema], &ctx).unwrap_err().to_string();
let err = validate(&[layer], &[schema]).unwrap_err().to_string();
assert_eq!(
err,
"Validation error: Layer 1: aesthetic 'y' references non-existent column 'missing'"
Expand All @@ -3488,9 +3463,8 @@ mod tests {
// Material aesthetics (color, size, etc.) should round-trip unchanged.
let layer = point_layer_with_mapping("color", "missing");
let schema: Schema = vec![col("present_x"), col("present_y")];
let ctx = Some(AestheticContext::from_static(&["x", "y"], &[]));

let err = validate(&[layer], &[schema], &ctx).unwrap_err().to_string();
let err = validate(&[layer], &[schema]).unwrap_err().to_string();
assert_eq!(
err,
"Validation error: Layer 1: aesthetic 'color' references non-existent column 'missing'"
Expand All @@ -3513,6 +3487,7 @@ mod tests {
// (only pos1 and the delayed pos2). The test here builds the
// post-transform state directly.
let mut layer = Layer::new(Geom::histogram());
layer.mappings = Mappings::new().with_cartesian_context();
layer.mappings.aesthetics.insert(
"pos1".to_string(),
AestheticValue::standard_column("present_x"),
Expand All @@ -3522,9 +3497,8 @@ mod tests {
AestheticValue::standard_column("count"),
);
let schema: Schema = vec![col("present_x")];
let ctx = Some(AestheticContext::from_static(&["x", "y"], &[]));

let err = validate(&[layer], &[schema], &ctx).unwrap_err().to_string();
let err = validate(&[layer], &[schema]).unwrap_err().to_string();
assert_eq!(
err,
"Validation error: Layer 1: REMAPPING targets unsupported aesthetic 'xmax' for geom 'histogram'"
Expand All @@ -3534,6 +3508,9 @@ mod tests {
#[test]
fn remapping_unsupported_target_translates_pos1max_to_anglemax_under_polar() {
let mut layer = Layer::new(Geom::histogram());
layer
.mappings
.set_context(AestheticContext::from_static(&["angle", "radius"], &[]));
layer.mappings.aesthetics.insert(
"pos1".to_string(),
AestheticValue::standard_column("present_x"),
Expand All @@ -3543,9 +3520,8 @@ mod tests {
AestheticValue::standard_column("count"),
);
let schema: Schema = vec![col("present_x")];
let ctx = Some(AestheticContext::from_static(&["angle", "radius"], &[]));

let err = validate(&[layer], &[schema], &ctx).unwrap_err().to_string();
let err = validate(&[layer], &[schema]).unwrap_err().to_string();
assert_eq!(
err,
"Validation error: Layer 1: REMAPPING targets unsupported aesthetic 'anglemax' for geom 'histogram'"
Expand Down
2 changes: 0 additions & 2 deletions src/execute/scale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ pub fn apply_post_stat_binning(
crate::plot::layer::geom::stat_aggregate::targeted_aesthetics(
&layer.parameters,
&layer.mappings,
&aesthetic_ctx,
)
})
.collect();
Expand Down Expand Up @@ -535,7 +534,6 @@ pub fn apply_pre_stat_resolve(spec: &mut Plot, layer_schemas: &[Schema]) -> Resu
for aes in crate::plot::layer::geom::stat_aggregate::targeted_aesthetics(
&layer.parameters,
&layer.mappings,
&aesthetic_ctx,
) {
targeted_in_any_layer.insert(aes);
}
Expand Down
2 changes: 0 additions & 2 deletions src/plot/layer/geom/area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ impl GeomTrait for Area {
parameters: &std::collections::HashMap<String, crate::plot::ParameterValue>,
_execute_query: &dyn Fn(&str) -> crate::Result<crate::DataFrame>,
dialect: &dyn crate::reader::SqlDialect,
aesthetic_ctx: &crate::plot::aesthetic::AestheticContext,
) -> crate::Result<StatResult> {
let result = if has_aggregate_param(parameters) {
stat_aggregate::apply(
Expand All @@ -79,7 +78,6 @@ impl GeomTrait for Area {
group_by,
parameters,
dialect,
aesthetic_ctx,
self.aggregate_domain_aesthetics().unwrap_or(&[]),
)?
} else {
Expand Down
2 changes: 0 additions & 2 deletions src/plot/layer/geom/bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ impl GeomTrait for Bar {
parameters: &HashMap<String, ParameterValue>,
_execute_query: &dyn Fn(&str) -> Result<DataFrame>,
dialect: &dyn SqlDialect,
aesthetic_ctx: &crate::plot::aesthetic::AestheticContext,
) -> Result<StatResult> {
let inner = if has_aggregate_param(parameters) {
stat_aggregate::apply(
Expand All @@ -103,7 +102,6 @@ impl GeomTrait for Bar {
group_by,
parameters,
dialect,
aesthetic_ctx,
self.aggregate_domain_aesthetics().unwrap_or(&[]),
)?
} else {
Expand Down
Loading
Loading