Skip to content

Commit 30aa6da

Browse files
committed
Resolve graph metric dependency owners
1 parent fce0ca2 commit 30aa6da

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

sidemantic-rs/src/sql/generator.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,31 @@ impl<'a> SqlGenerator<'a> {
819819
}
820820

821821
fn graph_metric_owner_models(&self, reference: &str, metric: &Metric) -> Result<Vec<String>> {
822+
let mut visiting = HashSet::new();
823+
self.graph_metric_owner_models_inner(reference, metric, &mut visiting)
824+
}
825+
826+
fn graph_metric_owner_models_inner(
827+
&self,
828+
reference: &str,
829+
metric: &Metric,
830+
visiting: &mut HashSet<String>,
831+
) -> Result<Vec<String>> {
832+
if !visiting.insert(reference.to_string()) {
833+
return Ok(Vec::new());
834+
}
835+
836+
let result = self.graph_metric_owner_models_uncycled(reference, metric, visiting);
837+
visiting.remove(reference);
838+
result
839+
}
840+
841+
fn graph_metric_owner_models_uncycled(
842+
&self,
843+
reference: &str,
844+
metric: &Metric,
845+
visiting: &mut HashSet<String>,
846+
) -> Result<Vec<String>> {
822847
let mut owners = HashSet::new();
823848

824849
for fragment in [
@@ -838,6 +863,13 @@ impl<'a> SqlGenerator<'a> {
838863
{
839864
self.collect_owner_models_from_fragment(fragment, &mut owners);
840865
}
866+
for fragment in self.graph_metric_dependency_fragments(metric) {
867+
self.collect_owner_models_from_graph_metric_dependencies(
868+
fragment,
869+
&mut owners,
870+
visiting,
871+
)?;
872+
}
841873

842874
for filter in &metric.filters {
843875
self.collect_owner_models_from_fragment(filter, &mut owners);
@@ -890,6 +922,23 @@ impl<'a> SqlGenerator<'a> {
890922
Ok(owners)
891923
}
892924

925+
fn graph_metric_dependency_fragments<'b>(&self, metric: &'b Metric) -> Vec<&'b str> {
926+
match metric.r#type {
927+
MetricType::Derived => metric.sql.iter().map(String::as_str).collect(),
928+
MetricType::Ratio => [metric.numerator.as_deref(), metric.denominator.as_deref()]
929+
.into_iter()
930+
.flatten()
931+
.collect(),
932+
MetricType::Cumulative | MetricType::TimeComparison => {
933+
[metric.base_metric.as_deref(), metric.sql.as_deref()]
934+
.into_iter()
935+
.flatten()
936+
.collect()
937+
}
938+
_ => Vec::new(),
939+
}
940+
}
941+
893942
fn collect_owner_models_from_fragment(&self, fragment: &str, owners: &mut HashSet<String>) {
894943
let model_ref_re =
895944
regex::Regex::new(r"\b([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\b")
@@ -905,6 +954,34 @@ impl<'a> SqlGenerator<'a> {
905954
}
906955
}
907956

957+
fn collect_owner_models_from_graph_metric_dependencies(
958+
&self,
959+
fragment: &str,
960+
owners: &mut HashSet<String>,
961+
visiting: &mut HashSet<String>,
962+
) -> Result<()> {
963+
let metric_ref_re = regex::Regex::new(
964+
r"\b([A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*|[A-Za-z_][A-Za-z0-9_]*)\b",
965+
)
966+
.expect("valid metric reference regex");
967+
for cap in metric_ref_re.captures_iter(fragment) {
968+
let Some(token_match) = cap.get(1) else {
969+
continue;
970+
};
971+
let token = token_match.as_str();
972+
if token.contains('.') || Self::is_sql_keyword_or_function(token) {
973+
continue;
974+
}
975+
let Some(metric) = self.graph.get_metric(token) else {
976+
continue;
977+
};
978+
for owner in self.graph_metric_owner_models_inner(token, metric, visiting)? {
979+
owners.insert(owner);
980+
}
981+
}
982+
Ok(())
983+
}
984+
908985
fn metric_for_ref(&self, metric_ref: &MetricRef) -> Result<&Metric> {
909986
self.metric_for_model_with_source(
910987
&metric_ref.model,
@@ -4685,6 +4762,36 @@ mod tests {
46854762
assert!(!sql.contains("FROM orders"), "{sql}");
46864763
}
46874764

4765+
#[test]
4766+
fn test_graph_metric_owner_follows_unqualified_graph_metric_dependencies() {
4767+
let mut graph = create_test_graph();
4768+
graph
4769+
.add_metric(Metric::sum("signups", "orders.signups"))
4770+
.unwrap();
4771+
graph
4772+
.add_metric(Metric::sum("visitors", "orders.visitors"))
4773+
.unwrap();
4774+
graph
4775+
.add_metric(Metric::ratio("conversion_rate", "signups", "visitors"))
4776+
.unwrap();
4777+
let generator = SqlGenerator::new(&graph);
4778+
4779+
let refs = generator
4780+
.parse_metric_refs(&["conversion_rate".to_string()])
4781+
.unwrap();
4782+
4783+
assert_eq!(refs.len(), 1);
4784+
assert_eq!(refs[0].model, "orders");
4785+
assert_eq!(refs[0].name, "conversion_rate");
4786+
assert!(refs[0].graph_metric);
4787+
4788+
let query = SemanticQuery::new().with_metrics(vec!["conversion_rate".into()]);
4789+
let sql = generator.generate(&query).unwrap();
4790+
4791+
assert!(sql.contains("orders.signups AS signups_raw"), "{sql}");
4792+
assert!(sql.contains("orders.visitors AS visitors_raw"), "{sql}");
4793+
}
4794+
46884795
#[test]
46894796
fn test_qualified_model_metric_wins_over_same_name_graph_metric() {
46904797
let mut graph = create_test_graph();

0 commit comments

Comments
 (0)