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
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,53 @@ impl AggregationInfo {
pub fn get_args(&self) -> &Vec<String> {
&self.args
}

/// Returns true if this aggregation matches the given template
/// (same function name, value column, and arguments).
pub fn matches_pattern(&self, other: &AggregationInfo) -> bool {
self.name == other.name
&& self.value_column_name == other.value_column_name
&& self.args == other.args
}
}

impl TimeInfo {
/// Returns true if this time info matches the given template.
///
/// For "UNUSED" time columns (the outer level of a subquery which has no WHERE
/// time clause), only the column name is compared.
/// For real time columns, the column name and duration are compared but the
/// absolute start time is ignored — this allows NOW()-based templates to match
/// incoming queries that use absolute timestamps.
pub fn matches_pattern(&self, other: &TimeInfo) -> bool {
if self.time_col_name != other.time_col_name {
return false;
}
if self.time_col_name == "UNUSED" {
return true;
}
(self.duration - other.duration).abs() < f64::EPSILON
}
}

impl SQLQueryData {
/// Returns true if this query data structurally matches the given template.
///
/// Templates in inference_config use NOW()-relative timestamps; actual incoming
/// queries use absolute timestamps. Only the duration is compared, not the
/// absolute start time. All other fields (metric, aggregation, labels, time
/// column name) must match exactly.
pub fn matches_sql_pattern(&self, template: &SQLQueryData) -> bool {
self.metric == template.metric
&& self
.aggregation_info
.matches_pattern(&template.aggregation_info)
&& self.labels == template.labels
&& self.time_info.matches_pattern(&template.time_info)
&& match (&self.subquery, &template.subquery) {
(None, None) => true,
(Some(sq), Some(tq)) => sq.matches_sql_pattern(tq),
_ => false,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -544,4 +544,99 @@ mod tests {
Some(QueryError::SpatialDurationSmall),
);
}

// ── matches_sql_pattern tests ─────────────────────────────────────────────

#[test]
fn test_matches_now_vs_absolute_timestamp() {
// Same 10s window, same metric/agg/labels — should match
let template = parse_sql_query(
"SELECT SUM(value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
let incoming = parse_sql_query(
"SELECT SUM(value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, '2025-10-01 00:00:10') AND '2025-10-01 00:00:10' GROUP BY L1, L2, L3, L4"
).unwrap();
assert!(incoming.matches_sql_pattern(&template));
}

#[test]
fn test_no_match_different_duration() {
let template = parse_sql_query(
"SELECT SUM(value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
let incoming = parse_sql_query(
"SELECT SUM(value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -5, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
assert!(!incoming.matches_sql_pattern(&template));
}

#[test]
fn test_no_match_different_metric() {
let template = parse_sql_query(
"SELECT SUM(value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
let incoming = parse_sql_query(
"SELECT SUM(mb) FROM mem_usage WHERE ms BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
assert!(!incoming.matches_sql_pattern(&template));
}

#[test]
fn test_no_match_different_aggregation() {
let template = parse_sql_query(
"SELECT AVG(value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
let incoming = parse_sql_query(
"SELECT SUM(value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
assert!(!incoming.matches_sql_pattern(&template));
}

#[test]
fn test_no_match_different_labels() {
let template = parse_sql_query(
"SELECT SUM(value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
let incoming = parse_sql_query(
"SELECT SUM(value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1"
).unwrap();
assert!(!incoming.matches_sql_pattern(&template));
}

#[test]
fn test_no_match_different_time_column() {
// cpu_usage uses "time", mem_usage uses "ms" — query same metric but wrong time col
let template = parse_sql_query(
"SELECT SUM(value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
// Force a different time column by using mem_usage schema (col: ms) but same duration
let incoming = parse_sql_query(
"SELECT SUM(mb) FROM mem_usage WHERE ms BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
// Different metric AND time column — must not match
assert!(!incoming.matches_sql_pattern(&template));
}

#[test]
fn test_no_match_different_quantile_args() {
let template = parse_sql_query(
"SELECT QUANTILE(0.95, value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
let incoming = parse_sql_query(
"SELECT QUANTILE(0.99, value) FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4"
).unwrap();
assert!(!incoming.matches_sql_pattern(&template));
}

#[test]
fn test_matches_subquery_now_vs_absolute() {
// Spatial-of-temporal: outer has no time clause (UNUSED), inner has time clause
let template = parse_sql_query(
"SELECT SUM(result) FROM (SELECT SUM(value) AS result FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, NOW()) AND NOW() GROUP BY L1, L2, L3, L4) GROUP BY L1"
).unwrap();
let incoming = parse_sql_query(
"SELECT SUM(result) FROM (SELECT SUM(value) AS result FROM cpu_usage WHERE time BETWEEN DATEADD(s, -10, '2025-10-01 00:00:10') AND '2025-10-01 00:00:10' GROUP BY L1, L2, L3, L4) GROUP BY L1"
).unwrap();
assert!(incoming.matches_sql_pattern(&template));
}
}
37 changes: 32 additions & 5 deletions asap-query-engine/src/engines/simple_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use promql_utilities::query_logics::parsing::{

use sql_utilities::ast_matching::QueryType;
use sql_utilities::ast_matching::{SQLPatternMatcher, SQLPatternParser, SQLQuery};
use sql_utilities::sqlhelper::AggregationInfo;
use sql_utilities::sqlhelper::{AggregationInfo, SQLQueryData};
use sqlparser::dialect::*;
use sqlparser::parser::Parser as parser;

Expand Down Expand Up @@ -292,6 +292,33 @@ impl SimpleEngine {
.find(|config| config.query == query)
}

/// Finds the query configuration for a SQL query using structural pattern matching.
///
/// Unlike `find_query_config` (which does exact string comparison), this method parses
/// each template in query_configs and compares it structurally against the incoming
/// query_data — ignoring absolute timestamps and comparing only metric, aggregation,
/// labels, time column name, and duration.
fn find_query_config_sql(&self, query_data: &SQLQueryData) -> Option<&QueryConfig> {
let schema = match &self.inference_config.schema {
SchemaConfig::SQL(sql_schema) => sql_schema,
_ => return None,
};

self.inference_config.query_configs.iter().find(|config| {
let template_statements =
match parser::parse_sql(&GenericDialect {}, config.query.as_str()) {
Ok(stmts) => stmts,
Err(_) => return false,
};
let template_data =
match SQLPatternParser::new(schema, 0.0).parse_query(&template_statements) {
Some(data) => data,
None => return false,
};
query_data.matches_sql_pattern(&template_data)
})
}

/// Validates and potentially aligns end timestamp based on query pattern
fn validate_and_align_end_timestamp(
&self,
Expand Down Expand Up @@ -1134,7 +1161,7 @@ impl SimpleEngine {
let query_time = Self::convert_query_time_to_data_time(
query_data.time_info.get_start() + query_data.time_info.get_duration(),
);
return self.build_spatiotemporal_context(&match_result, query_time, &query);
return self.build_spatiotemporal_context(&match_result, query_time, &query_data);
}

let query_pattern_type = match &match_result.query_type[..] {
Expand All @@ -1156,7 +1183,7 @@ impl SimpleEngine {
_ => panic!("Unsupported query type found"),
};

let query_config = self.find_query_config(&query)?;
let query_config = self.find_query_config_sql(&query_data)?;

// For nested queries (spatial of temporal), the outer query has no time clause,
// so we need to use the inner (temporal) query's time_info to compute query_time
Expand Down Expand Up @@ -1341,9 +1368,9 @@ impl SimpleEngine {
&self,
match_result: &SQLQuery,
query_time: u64,
query: &str,
query_data: &SQLQueryData,
) -> Option<QueryExecutionContext> {
let query_config = self.find_query_config(query)?;
let query_config = self.find_query_config_sql(query_data)?;

// Output labels are the GROUP BY columns (subset of all labels)
let query_output_labels = KeyByLabelNames::new(
Expand Down
1 change: 1 addition & 0 deletions asap-query-engine/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod datafusion;
pub mod elastic_forwarding_tests;
pub mod prometheus_forwarding_tests;
pub mod query_equivalence_tests;
pub mod sql_pattern_matching_tests;
pub mod trait_design_tests;

#[cfg(test)]
Expand Down
Loading
Loading