use crate::plot::types::DefaultAestheticValue;
use crate::{naming, DataFrame, Mappings, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
pub mod types;
mod area;
mod arrow;
mod bar;
mod boxplot;
mod density;
mod histogram;
mod line;
mod path;
mod point;
mod polygon;
mod range;
mod ribbon;
mod rule;
mod segment;
mod smooth;
mod spatial;
pub(crate) mod stat_aggregate;
mod text;
mod tile;
mod violin;
pub use types::{
DefaultAesthetics, DefaultParamValue, ParamConstraint, ParamDefinition, StatResult,
};
pub use area::Area;
pub use arrow::Arrow;
pub use bar::Bar;
pub use boxplot::Boxplot;
pub use density::Density;
pub use histogram::Histogram;
pub use line::Line;
pub use path::Path;
pub use point::Point;
pub use polygon::Polygon;
pub use range::Range;
pub use ribbon::Ribbon;
pub use rule::Rule;
pub use segment::Segment;
pub use smooth::Smooth;
pub use spatial::Spatial;
pub use text::Text;
pub use tile::Tile;
pub use violin::Violin;
use crate::plot::aesthetic::AestheticContext;
use crate::plot::projection::Projection;
use crate::plot::types::{ParameterValue, Parameters, Schema};
use crate::reader::SqlDialect;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GeomType {
Point,
Line,
Path,
Bar,
Area,
Tile,
Polygon,
Ribbon,
Histogram,
Density,
Smooth,
Boxplot,
Violin,
Text,
Segment,
Arrow,
Rule,
Range,
Spatial,
}
impl std::fmt::Display for GeomType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
GeomType::Point => "point",
GeomType::Line => "line",
GeomType::Path => "path",
GeomType::Bar => "bar",
GeomType::Area => "area",
GeomType::Tile => "tile",
GeomType::Polygon => "polygon",
GeomType::Ribbon => "ribbon",
GeomType::Histogram => "histogram",
GeomType::Density => "density",
GeomType::Smooth => "smooth",
GeomType::Boxplot => "boxplot",
GeomType::Violin => "violin",
GeomType::Text => "text",
GeomType::Segment => "segment",
GeomType::Arrow => "arrow",
GeomType::Rule => "rule",
GeomType::Range => "range",
GeomType::Spatial => "spatial",
};
write!(f, "{}", s)
}
}
pub trait GeomTrait: std::fmt::Debug + std::fmt::Display + Send + Sync {
fn geom_type(&self) -> GeomType;
fn aesthetics(&self) -> DefaultAesthetics;
fn validate_aesthetics(
&self,
_mappings: &crate::Mappings,
_aesthetic_ctx: &Option<AestheticContext>,
_parameters: &HashMap<String, ParameterValue>,
) -> std::result::Result<(), String> {
Ok(())
}
fn default_remappings(&self) -> DefaultAesthetics {
DefaultAesthetics { defaults: &[] }
}
fn valid_stat_columns(&self) -> &'static [&'static str] {
&[]
}
fn default_params(&self) -> &'static [ParamDefinition] {
&[]
}
fn stat_consumed_aesthetics(&self) -> &'static [&'static str] {
&[]
}
fn aggregate_domain_aesthetics(&self) -> Option<&'static [&'static str]> {
None
}
fn supports_aggregate(&self) -> bool {
self.aggregate_domain_aesthetics().is_some()
}
#[allow(clippy::too_many_arguments)]
fn apply_stat_transform(
&self,
query: &str,
schema: &Schema,
aesthetics: &Mappings,
group_by: &[String],
parameters: &Parameters,
_execute_query: &dyn Fn(&str) -> Result<DataFrame>,
dialect: &dyn SqlDialect,
aesthetic_ctx: &AestheticContext,
) -> Result<StatResult> {
let mut result = if let (Some(domain), true) = (
self.aggregate_domain_aesthetics(),
has_aggregate_param(parameters),
) {
stat_aggregate::apply(
query,
schema,
aesthetics,
group_by,
parameters,
dialect,
aesthetic_ctx,
domain,
)?
} else {
StatResult::Identity
};
let aes = self.aesthetics();
for axis in aes.dummy_axes() {
if !types::axis_family_has_mapping(aesthetics, axis) {
result = types::wrap_stat_with_dummy_axis(query, result, axis);
}
}
Ok(result)
}
fn post_process(&self, df: DataFrame, _parameters: &Parameters) -> Result<DataFrame> {
Ok(df)
}
fn apply_projection(
&self,
query: &str,
projection: &Projection,
_dialect: &dyn SqlDialect,
_mappings: &mut Mappings,
_partition_by: &mut Vec<String>,
_parameters: &mut std::collections::HashMap<String, ParameterValue>,
) -> Result<String> {
if needs_projection(projection) {
return Err(crate::GgsqlError::ValidationError(format!(
"Layer '{}' is not supported under '{}' projection.",
self.geom_type(),
projection.coord.name()
)));
}
Ok(query.to_string())
}
fn setup_layer(&self, _mappings: &mut Mappings, _parameters: &mut Parameters) -> Result<()> {
Ok(())
}
fn valid_settings(&self) -> Vec<&'static str> {
let mut valid: Vec<&'static str> = self.aesthetics().supported();
for param in self.default_params() {
valid.push(param.name);
}
valid
}
}
pub(crate) fn project_position_columns(
query: &str,
projection: &Projection,
dialect: &dyn SqlDialect,
columns: &[String],
) -> Result<String> {
use crate::plot::projection::coord::CoordKind;
if projection.coord.coord_kind() != CoordKind::Map {
return Ok(query.to_string());
}
let target = match projection.properties.get("target") {
Some(ParameterValue::String(s)) => s.as_str(),
_ => return Ok(query.to_string()),
};
let source = match projection.properties.get("source") {
Some(ParameterValue::String(s)) => s.as_str(),
_ => "EPSG:4326",
};
if source == target {
return Ok(query.to_string());
}
let pos1 = naming::quote_ident(&naming::aesthetic_column("pos1"));
let pos2 = naming::quote_ident(&naming::aesthetic_column("pos2"));
let point_expr = format!("ST_Point({pos1}, {pos2})");
let transformed = dialect.sql_st_transform(&point_expr, source, target);
let proj_col = naming::quote_ident("__ggsql_proj_pt__");
let inner = format!("SELECT *, {transformed} AS {proj_col} FROM ({query})");
let x_expr = format!("ST_X({proj_col})");
let y_expr = format!("ST_Y({proj_col})");
if columns.is_empty() {
return Ok(format!(
"SELECT {x_expr} AS {pos1}, {y_expr} AS {pos2}, * \
FROM ({inner}) \"__ggsql_pp__\""
));
}
let select_list: Vec<String> = columns
.iter()
.map(|c| {
let qc = naming::quote_ident(c);
if qc == pos1 {
format!("{x_expr} AS {pos1}")
} else if qc == pos2 {
format!("{y_expr} AS {pos2}")
} else {
qc
}
})
.collect();
Ok(format!(
"SELECT {} FROM ({inner}) \"__ggsql_pp__\"",
select_list.join(", ")
))
}
pub(crate) fn needs_projection(projection: &Projection) -> bool {
use crate::plot::projection::coord::CoordKind;
if projection.coord.coord_kind() != CoordKind::Map {
return false;
}
let target = match projection.properties.get("target") {
Some(ParameterValue::String(s)) => s.as_str(),
_ => return false,
};
let source = match projection.properties.get("source") {
Some(ParameterValue::String(s)) => s.as_str(),
_ => return false,
};
source != target
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn densify_edges(
query: &str,
dialect: &dyn SqlDialect,
columns: &[String],
partition_by: &[String],
domain_order: Option<&str>,
close_ring: bool,
segment_length: f64,
n_segments: usize,
) -> String {
let pos1 = naming::quote_ident(&naming::aesthetic_column("pos1"));
let pos2 = naming::quote_ident(&naming::aesthetic_column("pos2"));
let pos1_col = naming::aesthetic_column("pos1");
let pos2_col = naming::aesthetic_column("pos2");
let continuous_cols: Vec<&String> = columns
.iter()
.filter(|c| *c != &pos1_col && *c != &pos2_col && !partition_by.contains(c))
.collect();
let order_col = match domain_order {
Some(col) => naming::quote_ident(col),
None => "\"__ggsql_edge_idx__\"".to_string(),
};
let partition_clause = if partition_by.is_empty() {
String::new()
} else {
let parts: Vec<String> = partition_by
.iter()
.map(|c| naming::quote_ident(c))
.collect();
format!("PARTITION BY {}", parts.join(", "))
};
let window_def = if partition_clause.is_empty() {
format!("ORDER BY {order_col}")
} else {
format!("{partition_clause} ORDER BY {order_col}")
};
let seq_cte = dialect.sql_generate_series(n_segments);
let indexed_query = if domain_order.is_none() {
format!(
"SELECT *, ROW_NUMBER() OVER ({partition_clause} ORDER BY (SELECT NULL)) \
AS \"__ggsql_edge_idx__\" FROM ({query})"
)
} else {
query.to_string()
};
let pos1_lead = if close_ring {
format!(
"COALESCE(LEAD({pos1}) OVER w, FIRST_VALUE({pos1}) OVER w) AS \"__ggsql_next_pos1__\""
)
} else {
format!("LEAD({pos1}) OVER w AS \"__ggsql_next_pos1__\"")
};
let pos2_lead = if close_ring {
format!(
"COALESCE(LEAD({pos2}) OVER w, FIRST_VALUE({pos2}) OVER w) AS \"__ggsql_next_pos2__\""
)
} else {
format!("LEAD({pos2}) OVER w AS \"__ggsql_next_pos2__\"")
};
let mut cont_leads = String::new();
for c in &continuous_cols {
let qc = naming::quote_ident(c);
let alias = format!("\"__ggsql_next_{}\"", c.replace('"', ""));
if close_ring {
cont_leads.push_str(&format!(
", COALESCE(LEAD({qc}) OVER w, FIRST_VALUE({qc}) OVER w) AS {alias}"
));
} else {
cont_leads.push_str(&format!(", LEAD({qc}) OVER w AS {alias}"));
}
}
let seg_len = format!(
"SQRT(POWER(\"__ggsql_next_pos1__\" - {pos1}, 2) + \
POWER(\"__ggsql_next_pos2__\" - {pos2}, 2))"
);
let edges_query = format!(
"SELECT *, {pos1_lead}, {pos2_lead}{cont_leads}, \
{seg_len} AS \"__ggsql_seg_len__\" \
FROM ({indexed_query}) \"__ggsql_src__\" \
WINDOW w AS ({window_def})"
);
let threshold_lit = format!("{:.6}", segment_length);
let n_subdivs = format!("CEIL(\"__ggsql_seg_len__\" / {threshold_lit})");
let mut select_parts: Vec<String> = Vec::new();
for c in partition_by {
select_parts.push(naming::quote_ident(c));
}
let frac = format!("CAST(\"__ggsql_seq__\".n AS REAL) / {n_subdivs}");
select_parts.push(format!(
"{pos1} + COALESCE((\"__ggsql_next_pos1__\" - {pos1}) * ({frac}), 0.0) AS {pos1}"
));
select_parts.push(format!(
"{pos2} + COALESCE((\"__ggsql_next_pos2__\" - {pos2}) * ({frac}), 0.0) AS {pos2}"
));
for c in &continuous_cols {
let qc = naming::quote_ident(c);
let next = format!("\"__ggsql_next_{}\"", c.replace('"', ""));
select_parts.push(format!(
"{qc} + COALESCE(({next} - {qc}) * ({frac}), 0.0) AS {qc}"
));
}
let where_clause = if close_ring {
format!("\"__ggsql_seq__\".n < {n_subdivs}")
} else {
format!(
"(\"__ggsql_next_pos1__\" IS NOT NULL AND \"__ggsql_seq__\".n < {n_subdivs}) \
OR (\"__ggsql_next_pos1__\" IS NULL AND \"__ggsql_seq__\".n = 0)"
)
};
let order_parts = if partition_by.is_empty() {
format!("{order_col}, \"__ggsql_seq__\".n")
} else {
let parts: Vec<String> = partition_by
.iter()
.map(|c| naming::quote_ident(c))
.collect();
format!("{}, {order_col}, \"__ggsql_seq__\".n", parts.join(", "))
};
format!(
"WITH {seq_cte}, \
\"__ggsql_edges__\" AS ({edges_query}) \
SELECT {select} \
FROM \"__ggsql_edges__\" \
CROSS JOIN \"__ggsql_seq__\" \
WHERE {where_clause} \
ORDER BY {order_parts}",
select = select_parts.join(", "),
)
}
pub(crate) fn has_aggregate_param(parameters: &Parameters) -> bool {
matches!(
parameters.get("aggregate"),
Some(ParameterValue::String(_)) | Some(ParameterValue::Array(_))
)
}
#[derive(Clone)]
pub struct Geom(Arc<dyn GeomTrait>);
impl Geom {
pub fn point() -> Self {
Self(Arc::new(Point))
}
pub fn line() -> Self {
Self(Arc::new(Line))
}
pub fn path() -> Self {
Self(Arc::new(Path))
}
pub fn bar() -> Self {
Self(Arc::new(Bar))
}
pub fn area() -> Self {
Self(Arc::new(Area))
}
pub fn tile() -> Self {
Self(Arc::new(Tile))
}
pub fn polygon() -> Self {
Self(Arc::new(Polygon))
}
pub fn ribbon() -> Self {
Self(Arc::new(Ribbon))
}
pub fn histogram() -> Self {
Self(Arc::new(Histogram))
}
pub fn density() -> Self {
Self(Arc::new(Density))
}
pub fn smooth() -> Self {
Self(Arc::new(Smooth))
}
pub fn boxplot() -> Self {
Self(Arc::new(Boxplot))
}
pub fn violin() -> Self {
Self(Arc::new(Violin))
}
pub fn text() -> Self {
Self(Arc::new(Text))
}
pub fn segment() -> Self {
Self(Arc::new(Segment))
}
pub fn arrow() -> Self {
Self(Arc::new(Arrow))
}
pub fn rule() -> Self {
Self(Arc::new(Rule))
}
pub fn range() -> Self {
Self(Arc::new(Range))
}
pub fn spatial() -> Self {
Self(Arc::new(Spatial))
}
pub fn from_type(t: GeomType) -> Self {
match t {
GeomType::Point => Self::point(),
GeomType::Line => Self::line(),
GeomType::Path => Self::path(),
GeomType::Bar => Self::bar(),
GeomType::Area => Self::area(),
GeomType::Tile => Self::tile(),
GeomType::Polygon => Self::polygon(),
GeomType::Ribbon => Self::ribbon(),
GeomType::Histogram => Self::histogram(),
GeomType::Density => Self::density(),
GeomType::Smooth => Self::smooth(),
GeomType::Boxplot => Self::boxplot(),
GeomType::Violin => Self::violin(),
GeomType::Text => Self::text(),
GeomType::Segment => Self::segment(),
GeomType::Arrow => Self::arrow(),
GeomType::Rule => Self::rule(),
GeomType::Range => Self::range(),
GeomType::Spatial => Self::spatial(),
}
}
pub fn geom_type(&self) -> GeomType {
self.0.geom_type()
}
pub fn aesthetics(&self) -> DefaultAesthetics {
self.0.aesthetics()
}
pub fn default_remappings(&self) -> DefaultAesthetics {
self.0.default_remappings()
}
pub fn implicit_default_remappings(&self) -> Vec<(&'static str, DefaultAestheticValue)> {
let explicit = self.0.default_remappings();
let mut out: Vec<(&'static str, DefaultAestheticValue)> = explicit.defaults.to_vec();
for axis in self.0.aesthetics().dummy_axes() {
if !out.iter().any(|(name, _)| *name == axis) {
out.push((axis, DefaultAestheticValue::Column(axis)));
}
}
out
}
pub fn valid_stat_columns(&self) -> &'static [&'static str] {
self.0.valid_stat_columns()
}
pub fn implicit_valid_stat_columns(&self) -> Vec<&'static str> {
let explicit = self.0.valid_stat_columns();
let mut out: Vec<&'static str> = explicit.to_vec();
for axis in self.0.aesthetics().dummy_axes() {
if !out.contains(&axis) {
out.push(axis);
}
}
out
}
pub fn default_params(&self) -> &'static [ParamDefinition] {
self.0.default_params()
}
pub fn stat_consumed_aesthetics(&self) -> &'static [&'static str] {
self.0.stat_consumed_aesthetics()
}
#[allow(clippy::too_many_arguments)]
pub fn apply_stat_transform(
&self,
query: &str,
schema: &Schema,
aesthetics: &Mappings,
group_by: &[String],
parameters: &Parameters,
execute_query: &dyn Fn(&str) -> Result<DataFrame>,
dialect: &dyn SqlDialect,
aesthetic_ctx: &AestheticContext,
) -> Result<StatResult> {
self.0.apply_stat_transform(
query,
schema,
aesthetics,
group_by,
parameters,
execute_query,
dialect,
aesthetic_ctx,
)
}
pub fn post_process(&self, df: DataFrame, parameters: &Parameters) -> Result<DataFrame> {
self.0.post_process(df, parameters)
}
pub fn apply_projection(
&self,
query: &str,
projection: &Projection,
dialect: &dyn SqlDialect,
mappings: &mut Mappings,
partition_by: &mut Vec<String>,
parameters: &mut std::collections::HashMap<String, ParameterValue>,
) -> Result<String> {
self.0.apply_projection(
query,
projection,
dialect,
mappings,
partition_by,
parameters,
)
}
pub fn setup_layer(&self, mappings: &mut Mappings, parameters: &mut Parameters) -> Result<()> {
self.0.setup_layer(mappings, parameters)
}
pub fn valid_settings(&self) -> Vec<&'static str> {
self.0.valid_settings()
}
pub fn supports_aggregate(&self) -> bool {
self.0.supports_aggregate()
}
pub fn aggregate_domain_aesthetics(&self) -> Option<&'static [&'static str]> {
self.0.aggregate_domain_aesthetics()
}
pub fn validate_aesthetics(
&self,
mappings: &Mappings,
aesthetic_ctx: &Option<AestheticContext>,
parameters: &HashMap<String, ParameterValue>,
) -> std::result::Result<(), String> {
self.0
.validate_aesthetics(mappings, aesthetic_ctx, parameters)
}
}
impl std::fmt::Debug for Geom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Geom::{:?}", self.geom_type())
}
}
impl std::fmt::Display for Geom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl PartialEq for Geom {
fn eq(&self, other: &Self) -> bool {
self.geom_type() == other.geom_type()
}
}
impl Eq for Geom {}
impl Serialize for Geom {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.geom_type().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Geom {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let geom_type = GeomType::deserialize(deserializer)?;
Ok(Geom::from_type(geom_type))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_geom_creation() {
let point = Geom::point();
assert_eq!(point.geom_type(), GeomType::Point);
let line = Geom::line();
assert_eq!(line.geom_type(), GeomType::Line);
}
#[test]
fn test_geom_equality() {
let p1 = Geom::point();
let p2 = Geom::point();
let l1 = Geom::line();
assert_eq!(p1, p2);
assert_ne!(p1, l1);
}
#[test]
fn test_geom_display() {
assert_eq!(format!("{}", Geom::point()), "point");
assert_eq!(format!("{}", Geom::histogram()), "histogram");
}
#[test]
fn test_geom_type_display() {
assert_eq!(format!("{}", GeomType::Point), "point");
assert_eq!(format!("{}", GeomType::Range), "range");
}
#[test]
fn test_geom_from_type() {
let geom = Geom::from_type(GeomType::Bar);
assert_eq!(geom.geom_type(), GeomType::Bar);
}
#[test]
fn test_geom_aesthetics() {
let point = Geom::point();
let aes = point.aesthetics();
assert!(!aes.is_required("pos1"));
assert!(!aes.is_required("pos2"));
}
#[test]
fn test_geom_serialization() {
let point = Geom::point();
let json = serde_json::to_string(&point).unwrap();
assert_eq!(json, "\"point\"");
let deserialized: Geom = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.geom_type(), GeomType::Point);
}
#[test]
fn test_default_remappings_are_in_aesthetics() {
let all_geom_types = [
GeomType::Point,
GeomType::Line,
GeomType::Path,
GeomType::Bar,
GeomType::Area,
GeomType::Tile,
GeomType::Polygon,
GeomType::Ribbon,
GeomType::Histogram,
GeomType::Density,
GeomType::Smooth,
GeomType::Boxplot,
GeomType::Violin,
GeomType::Text,
GeomType::Segment,
GeomType::Arrow,
GeomType::Rule,
GeomType::Range,
GeomType::Spatial,
];
let _exhaustive_check = |t: GeomType| match t {
GeomType::Point
| GeomType::Line
| GeomType::Path
| GeomType::Bar
| GeomType::Area
| GeomType::Tile
| GeomType::Polygon
| GeomType::Ribbon
| GeomType::Histogram
| GeomType::Density
| GeomType::Smooth
| GeomType::Boxplot
| GeomType::Violin
| GeomType::Text
| GeomType::Segment
| GeomType::Arrow
| GeomType::Rule
| GeomType::Range
| GeomType::Spatial => {}
};
for geom_type in all_geom_types {
let geom = Geom::from_type(geom_type);
let remappings = geom.default_remappings();
let aesthetics = geom.aesthetics();
let aesthetic_names: std::collections::HashSet<&str> =
aesthetics.defaults.iter().map(|(name, _)| *name).collect();
for (name, _) in remappings.defaults {
assert!(
aesthetic_names.contains(name),
"Geom '{}' has '{}' in default_remappings() but not in aesthetics().defaults. \
Add it as DefaultAestheticValue::Delayed if it's a stat-produced aesthetic.",
geom_type,
name
);
}
}
}
#[test]
fn test_needs_projection_false_for_cartesian() {
let projection = Projection::cartesian();
assert!(!needs_projection(&projection));
}
#[test]
fn test_needs_projection_false_without_target() {
let projection = Projection::map();
assert!(!needs_projection(&projection));
}
#[test]
fn test_needs_projection_false_without_source() {
let mut projection = Projection::map();
projection.properties.insert(
"target".to_string(),
ParameterValue::String("+proj=ortho".to_string()),
);
assert!(!needs_projection(&projection));
}
#[test]
fn test_needs_projection_false_when_same_crs() {
let mut projection = Projection::map();
projection.properties.insert(
"source".to_string(),
ParameterValue::String("EPSG:4326".to_string()),
);
projection.properties.insert(
"target".to_string(),
ParameterValue::String("EPSG:4326".to_string()),
);
assert!(!needs_projection(&projection));
}
#[test]
fn test_needs_projection_true_when_different_crs() {
let mut projection = Projection::map();
projection.properties.insert(
"source".to_string(),
ParameterValue::String("EPSG:4326".to_string()),
);
projection.properties.insert(
"target".to_string(),
ParameterValue::String("+proj=ortho".to_string()),
);
assert!(needs_projection(&projection));
}
#[test]
fn test_apply_projection_default_errors_for_unsupported_geom() {
let mut projection = Projection::map();
projection.properties.insert(
"source".to_string(),
ParameterValue::String("EPSG:4326".to_string()),
);
projection.properties.insert(
"target".to_string(),
ParameterValue::String("+proj=ortho".to_string()),
);
let geom = Geom::bar();
let result = geom.apply_projection(
"SELECT * FROM t",
&projection,
&crate::reader::AnsiDialect,
&mut Mappings::new(),
&mut vec![],
&mut std::collections::HashMap::new(),
);
let err = result.unwrap_err();
assert_eq!(
err.to_string(),
"Validation error: Layer 'bar' is not supported under 'Unknown' projection."
);
}
#[test]
fn test_densify_edges_basic_structure() {
use crate::reader::AnsiDialect;
let columns = vec![
naming::aesthetic_column("pos1"),
naming::aesthetic_column("pos2"),
];
let pos1_col = naming::aesthetic_column("pos1");
let result = densify_edges(
"SELECT * FROM t",
&AnsiDialect,
&columns,
&[],
Some(&pos1_col),
false,
1.0,
360,
);
assert!(result.contains("__ggsql_seq__"));
assert!(result.contains("LEAD("));
assert!(result.contains("__ggsql_seg_len__"));
assert!(result.contains("__ggsql_next_pos1__"));
assert!(result.contains("__ggsql_next_pos2__"));
}
#[test]
fn test_densify_edges_with_partition() {
use crate::reader::AnsiDialect;
let columns = vec![
naming::aesthetic_column("pos1"),
naming::aesthetic_column("pos2"),
naming::aesthetic_column("stroke"),
];
let partition_by = vec![naming::aesthetic_column("stroke")];
let pos1_col = naming::aesthetic_column("pos1");
let result = densify_edges(
"SELECT * FROM t",
&AnsiDialect,
&columns,
&partition_by,
Some(&pos1_col),
false,
1.0,
360,
);
assert!(result.contains("PARTITION BY"));
assert!(result.contains("__ggsql_aes_stroke__"));
}
#[test]
fn test_densify_edges_interpolates_continuous_aesthetics() {
use crate::reader::AnsiDialect;
let columns = vec![
naming::aesthetic_column("pos1"),
naming::aesthetic_column("pos2"),
naming::aesthetic_column("stroke"),
naming::aesthetic_column("opacity"),
];
let partition_by = vec![naming::aesthetic_column("stroke")];
let pos1_col = naming::aesthetic_column("pos1");
let result = densify_edges(
"SELECT * FROM t",
&AnsiDialect,
&columns,
&partition_by,
Some(&pos1_col),
false,
1.0,
360,
);
assert!(result.contains("__ggsql_next___ggsql_aes_opacity__"));
}
#[test]
fn test_densify_edges_close_ring() {
use crate::reader::AnsiDialect;
let columns = vec![
naming::aesthetic_column("pos1"),
naming::aesthetic_column("pos2"),
];
let result = densify_edges(
"SELECT * FROM t",
&AnsiDialect,
&columns,
&[],
None,
true,
1.0,
360,
);
assert!(result.contains("FIRST_VALUE("));
assert!(result.contains("__ggsql_edge_idx__"));
}
#[test]
fn test_densify_edges_open_keeps_last_vertex() {
use crate::reader::AnsiDialect;
let columns = vec![
naming::aesthetic_column("pos1"),
naming::aesthetic_column("pos2"),
];
let pos1_col = naming::aesthetic_column("pos1");
let result = densify_edges(
"SELECT * FROM t",
&AnsiDialect,
&columns,
&[],
Some(&pos1_col),
false,
1.0,
360,
);
assert!(result.contains("IS NULL AND"));
}
}