Skip to content

Commit 1b91738

Browse files
committed
Fix dotted graph metric SQL generation
1 parent 54225e0 commit 1b91738

4 files changed

Lines changed: 296 additions & 63 deletions

File tree

sidemantic-rs/src/core/graph.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,12 @@ impl SemanticGraph {
308308
)));
309309
}
310310

311+
if Self::metric_uses_inline_aggregation(metric)
312+
&& self.inline_aggregate_column_dependency_exists(&dependency)
313+
{
314+
continue;
315+
}
316+
311317
if self.metric_dependency_exists(&dependency)? {
312318
continue;
313319
}
@@ -339,6 +345,63 @@ impl SemanticGraph {
339345
.any(|model| model.get_metric(dependency).is_some()))
340346
}
341347

348+
fn metric_uses_inline_aggregation(metric: &Metric) -> bool {
349+
metric.r#type == MetricType::Derived
350+
&& metric
351+
.sql
352+
.as_deref()
353+
.is_some_and(Self::sql_has_inline_aggregation)
354+
}
355+
356+
fn sql_has_inline_aggregation(sql: &str) -> bool {
357+
let lower = sql.to_ascii_lowercase();
358+
let bytes = lower.as_bytes();
359+
let aggregate_names = [
360+
"sum",
361+
"avg",
362+
"count",
363+
"min",
364+
"max",
365+
"median",
366+
"stddev",
367+
"stddev_pop",
368+
"variance",
369+
"variance_pop",
370+
];
371+
372+
for name in aggregate_names {
373+
let mut start = 0;
374+
while let Some(offset) = lower[start..].find(name) {
375+
let name_start = start + offset;
376+
let name_end = name_start + name.len();
377+
let before_is_ident = name_start > 0
378+
&& (bytes[name_start - 1].is_ascii_alphanumeric()
379+
|| bytes[name_start - 1] == b'_');
380+
let after_is_ident = name_end < bytes.len()
381+
&& (bytes[name_end].is_ascii_alphanumeric() || bytes[name_end] == b'_');
382+
if before_is_ident || after_is_ident {
383+
start = name_end;
384+
continue;
385+
}
386+
387+
if lower[name_end..].trim_start().starts_with('(') {
388+
return true;
389+
}
390+
start = name_end;
391+
}
392+
}
393+
394+
false
395+
}
396+
397+
fn inline_aggregate_column_dependency_exists(&self, dependency: &str) -> bool {
398+
if let Some((model_name, _)) = dependency.rsplit_once('.') {
399+
return self.models.contains_key(model_name);
400+
}
401+
402+
self.models.len() == 1
403+
}
404+
342405
/// Get a graph-level metric by name.
343406
pub fn get_metric(&self, name: &str) -> Option<&Metric> {
344407
self.metrics

sidemantic-rs/src/sql/generator.rs

Lines changed: 162 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,8 @@ impl<'a> SqlGenerator<'a> {
308308
let available: Vec<&str> = self.graph.models().map(|m| m.name.as_str()).collect();
309309
SidemanticError::model_not_found(&model_name, &available)
310310
})?;
311-
let metric = model.get_metric(&metric_name).ok_or_else(|| {
312-
let available: Vec<&str> = model.metrics.iter().map(|m| m.name.as_str()).collect();
313-
SidemanticError::metric_not_found(&model_name, &metric_name, &available)
314-
})?;
315-
let raw_alias = format!("{metric_name}_raw");
311+
let metric = self.metric_for_model(&model_name, &metric_name)?;
312+
let raw_alias = self.metric_raw_alias(model, &metric_name, metric);
316313
let mut raw_expr =
317314
self.normalize_cte_source_expression(&self.metric_raw_expression(metric, model));
318315
if !metric.filters.is_empty() {
@@ -457,16 +454,13 @@ impl<'a> SqlGenerator<'a> {
457454
let available: Vec<&str> = self.graph.models().map(|m| m.name.as_str()).collect();
458455
SidemanticError::model_not_found(&metric_ref.model, &available)
459456
})?;
460-
let metric = model.get_metric(&metric_ref.name).ok_or_else(|| {
461-
let available: Vec<&str> = model.metrics.iter().map(|m| m.name.as_str()).collect();
462-
SidemanticError::metric_not_found(&metric_ref.model, &metric_ref.name, &available)
463-
})?;
457+
let metric = self.metric_for_model(&metric_ref.model, &metric_ref.name)?;
464458

465459
let alias = self.model_alias(&metric_ref.model);
466460
let use_symmetric = fan_out_at_risk.contains(&metric_ref.model);
467461
let output_alias =
468462
self.output_alias(&metric_ref.model, &metric_ref.alias, &alias_collisions);
469-
let raw_alias = format!("{}_raw", metric_ref.name);
463+
let raw_alias = self.metric_raw_alias(model, &metric_ref.name, metric);
470464
let raw_col = format!("{alias}.{}", self.quote_identifier(&raw_alias));
471465

472466
let sql_expr = match metric.r#type {
@@ -793,12 +787,136 @@ impl<'a> SqlGenerator<'a> {
793787
}
794788

795789
match owners.len() {
796-
0 => Ok(None),
797-
1 => Ok(Some((owners[0].clone(), reference.to_string()))),
798-
_ => Err(SidemanticError::InvalidReference {
790+
0 => {}
791+
1 => return Ok(Some((owners[0].clone(), reference.to_string()))),
792+
_ => {
793+
return Err(SidemanticError::InvalidReference {
794+
reference: reference.to_string(),
795+
});
796+
}
797+
}
798+
799+
if let Some(metric) = self.graph.get_metric(reference) {
800+
let graph_metric_owners = self.graph_metric_owner_models(reference, metric)?;
801+
return match graph_metric_owners.len() {
802+
0 => Ok(None),
803+
1 => Ok(Some((
804+
graph_metric_owners[0].clone(),
805+
reference.to_string(),
806+
))),
807+
_ => Err(SidemanticError::InvalidReference {
808+
reference: reference.to_string(),
809+
}),
810+
};
811+
}
812+
813+
Ok(None)
814+
}
815+
816+
fn graph_metric_owner_models(&self, reference: &str, metric: &Metric) -> Result<Vec<String>> {
817+
let mut owners = HashSet::new();
818+
for model in self.graph.models() {
819+
if model.get_metric(reference).is_some() {
820+
owners.insert(model.name.clone());
821+
}
822+
}
823+
824+
if owners.is_empty() {
825+
for fragment in [
826+
metric.sql.as_deref(),
827+
metric.numerator.as_deref(),
828+
metric.denominator.as_deref(),
829+
metric.base_metric.as_deref(),
830+
metric.entity.as_deref(),
831+
metric.base_event.as_deref(),
832+
metric.conversion_event.as_deref(),
833+
metric.cohort_event.as_deref(),
834+
metric.activity_event.as_deref(),
835+
metric.having.as_deref(),
836+
]
837+
.into_iter()
838+
.flatten()
839+
{
840+
self.collect_owner_models_from_fragment(fragment, &mut owners);
841+
}
842+
843+
if let Some(steps) = metric.steps.as_ref() {
844+
for step in steps {
845+
self.collect_owner_models_from_fragment(step, &mut owners);
846+
}
847+
}
848+
if let Some(inner_metrics) = metric.inner_metrics.as_ref() {
849+
for inner_metric in inner_metrics {
850+
if let Some(sql) = inner_metric.sql.as_deref() {
851+
self.collect_owner_models_from_fragment(sql, &mut owners);
852+
}
853+
}
854+
}
855+
if let Some(entity_dimensions) = metric.entity_dimensions.as_ref() {
856+
for dimension in entity_dimensions {
857+
self.collect_owner_models_from_fragment(dimension, &mut owners);
858+
}
859+
}
860+
}
861+
862+
if owners.is_empty() {
863+
let mut model_names: Vec<String> = self
864+
.graph
865+
.models()
866+
.map(|model| model.name.clone())
867+
.collect();
868+
if model_names.len() == 1 {
869+
owners.insert(model_names.pop().expect("single model name"));
870+
}
871+
}
872+
873+
let mut owners: Vec<String> = owners.into_iter().collect();
874+
owners.sort();
875+
if owners.len() > 1 {
876+
return Err(SidemanticError::InvalidReference {
799877
reference: reference.to_string(),
800-
}),
878+
});
879+
}
880+
Ok(owners)
881+
}
882+
883+
fn collect_owner_models_from_fragment(&self, fragment: &str, owners: &mut HashSet<String>) {
884+
let model_ref_re =
885+
regex::Regex::new(r"\b([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\b")
886+
.expect("valid model reference regex");
887+
for cap in model_ref_re.captures_iter(fragment) {
888+
let Some(model_match) = cap.get(1) else {
889+
continue;
890+
};
891+
let model_name = model_match.as_str();
892+
if self.graph.get_model(model_name).is_some() {
893+
owners.insert(model_name.to_string());
894+
}
895+
}
896+
}
897+
898+
fn metric_for_model(&self, model_name: &str, metric_name: &str) -> Result<&Metric> {
899+
let model = self.graph.get_model(model_name).ok_or_else(|| {
900+
let available: Vec<&str> = self.graph.models().map(|m| m.name.as_str()).collect();
901+
SidemanticError::model_not_found(model_name, &available)
902+
})?;
903+
if let Some(metric) = model.get_metric(metric_name) {
904+
return Ok(metric);
905+
}
906+
907+
if let Some(metric) = self.graph.get_metric(metric_name) {
908+
let owners = self.graph_metric_owner_models(metric_name, metric)?;
909+
if owners.iter().any(|owner| owner == model_name) {
910+
return Ok(metric);
911+
}
801912
}
913+
914+
let available: Vec<&str> = model.metrics.iter().map(|m| m.name.as_str()).collect();
915+
Err(SidemanticError::metric_not_found(
916+
model_name,
917+
metric_name,
918+
&available,
919+
))
802920
}
803921

804922
/// Find all models required by the query
@@ -852,14 +970,7 @@ impl<'a> SqlGenerator<'a> {
852970
return Ok(());
853971
}
854972

855-
let model = self.graph.get_model(&metric_ref.model).ok_or_else(|| {
856-
let available: Vec<&str> = self.graph.models().map(|m| m.name.as_str()).collect();
857-
SidemanticError::model_not_found(&metric_ref.model, &available)
858-
})?;
859-
let metric = model.get_metric(&metric_ref.name).ok_or_else(|| {
860-
let available: Vec<&str> = model.metrics.iter().map(|m| m.name.as_str()).collect();
861-
SidemanticError::metric_not_found(&metric_ref.model, &metric_ref.name, &available)
862-
})?;
973+
let metric = self.metric_for_model(&metric_ref.model, &metric_ref.name)?;
863974

864975
let exprs: Vec<&str> = [
865976
metric.sql.as_deref(),
@@ -991,6 +1102,29 @@ impl<'a> SqlGenerator<'a> {
9911102
}
9921103
}
9931104

1105+
fn metric_raw_alias(&self, model: &Model, metric_name: &str, metric: &Metric) -> String {
1106+
if metric_name.contains('.') && metric.r#type == MetricType::Simple {
1107+
if let Some(column_name) = self.simple_metric_source_column(model, metric) {
1108+
return column_name;
1109+
}
1110+
}
1111+
format!("{metric_name}_raw")
1112+
}
1113+
1114+
fn simple_metric_source_column(&self, model: &Model, metric: &Metric) -> Option<String> {
1115+
let sql = metric.sql.as_deref()?.trim();
1116+
if Self::is_simple_identifier(sql) {
1117+
return Some(sql.to_string());
1118+
}
1119+
1120+
let (model_name, field_name) = sql.split_once('.')?;
1121+
if model_name == model.name && Self::is_simple_identifier(field_name) {
1122+
return Some(field_name.to_string());
1123+
}
1124+
1125+
None
1126+
}
1127+
9941128
fn collect_simple_metric_dependencies(
9951129
&self,
9961130
metric_ref: &MetricRef,
@@ -1002,14 +1136,7 @@ impl<'a> SqlGenerator<'a> {
10021136
return Ok(());
10031137
}
10041138

1005-
let model = self.graph.get_model(&metric_ref.model).ok_or_else(|| {
1006-
let available: Vec<&str> = self.graph.models().map(|m| m.name.as_str()).collect();
1007-
SidemanticError::model_not_found(&metric_ref.model, &available)
1008-
})?;
1009-
let metric = model.get_metric(&metric_ref.name).ok_or_else(|| {
1010-
let available: Vec<&str> = model.metrics.iter().map(|m| m.name.as_str()).collect();
1011-
SidemanticError::metric_not_found(&metric_ref.model, &metric_ref.name, &available)
1012-
})?;
1139+
let metric = self.metric_for_model(&metric_ref.model, &metric_ref.name)?;
10131140

10141141
match metric.r#type {
10151142
MetricType::Simple => {
@@ -1090,14 +1217,7 @@ impl<'a> SqlGenerator<'a> {
10901217
return Ok(());
10911218
}
10921219

1093-
let model = self.graph.get_model(&metric_ref.model).ok_or_else(|| {
1094-
let available: Vec<&str> = self.graph.models().map(|m| m.name.as_str()).collect();
1095-
SidemanticError::model_not_found(&metric_ref.model, &available)
1096-
})?;
1097-
let metric = model.get_metric(&metric_ref.name).ok_or_else(|| {
1098-
let available: Vec<&str> = model.metrics.iter().map(|m| m.name.as_str()).collect();
1099-
SidemanticError::metric_not_found(&metric_ref.model, &metric_ref.name, &available)
1100-
})?;
1220+
let metric = self.metric_for_model(&metric_ref.model, &metric_ref.name)?;
11011221

11021222
match metric.r#type {
11031223
MetricType::Derived if Self::is_inline_aggregate_expression(metric.sql_expr()) => {
@@ -1356,14 +1476,7 @@ impl<'a> SqlGenerator<'a> {
13561476

13571477
fn has_cumulative_metrics(&self, metric_refs: &[MetricRef]) -> Result<bool> {
13581478
for metric_ref in metric_refs {
1359-
let model = self.graph.get_model(&metric_ref.model).ok_or_else(|| {
1360-
let available: Vec<&str> = self.graph.models().map(|m| m.name.as_str()).collect();
1361-
SidemanticError::model_not_found(&metric_ref.model, &available)
1362-
})?;
1363-
let metric = model.get_metric(&metric_ref.name).ok_or_else(|| {
1364-
let available: Vec<&str> = model.metrics.iter().map(|m| m.name.as_str()).collect();
1365-
SidemanticError::metric_not_found(&metric_ref.model, &metric_ref.name, &available)
1366-
})?;
1479+
let metric = self.metric_for_model(&metric_ref.model, &metric_ref.name)?;
13671480
if metric.r#type == MetricType::Cumulative
13681481
|| metric.r#type == MetricType::TimeComparison
13691482
|| metric.r#type == MetricType::Conversion
@@ -4029,7 +4142,8 @@ impl<'a> SqlGenerator<'a> {
40294142
metric_name: &str,
40304143
alias: &str,
40314144
) -> String {
4032-
let raw_col = format!("{alias}.{metric_name}_raw");
4145+
let raw_alias = format!("{metric_name}_raw");
4146+
let raw_col = format!("{alias}.{}", self.quote_identifier(&raw_alias));
40334147
match metric.agg.as_ref() {
40344148
Some(Aggregation::CountDistinct) => format!("COUNT(DISTINCT {raw_col})"),
40354149
Some(Aggregation::Count) => format!("COUNT({raw_col})"),
@@ -4078,14 +4192,7 @@ impl<'a> SqlGenerator<'a> {
40784192
return Ok(None);
40794193
}
40804194

4081-
let model = self.graph.get_model(&model_name).ok_or_else(|| {
4082-
let available: Vec<&str> = self.graph.models().map(|m| m.name.as_str()).collect();
4083-
SidemanticError::model_not_found(&model_name, &available)
4084-
})?;
4085-
let metric = model.get_metric(&metric_name).ok_or_else(|| {
4086-
let available: Vec<&str> = model.metrics.iter().map(|m| m.name.as_str()).collect();
4087-
SidemanticError::metric_not_found(&model_name, &metric_name, &available)
4088-
})?;
4195+
let metric = self.metric_for_model(&model_name, &metric_name)?;
40894196

40904197
let alias = self.model_alias(&model_name);
40914198
let expanded = match metric.r#type {

0 commit comments

Comments
 (0)