feat: supports expression in TQL params (#7014)

* feat: supports expression in TQL params

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* chore: by cr comments

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* chore: comment

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

* chore: by cr comments

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>

---------

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
This commit is contained in:
dennis zhuang
2025-09-26 03:43:50 +00:00
committed by GitHub
parent c1acce9943
commit 17d16da483
5 changed files with 299 additions and 123 deletions
+134 -93
View File
@@ -34,7 +34,6 @@ const FORMAT: &str = "FORMAT";
use sqlparser::parser::Parser;
use crate::dialect::GreptimeDbDialect;
use crate::parsers::error::{
ConvertToLogicalExpressionSnafu, EvaluationSnafu, ParserSnafu, TQLError,
};
@@ -106,36 +105,49 @@ impl ParserContext<'_> {
let (start, end, step, lookback) = match parser.peek_token().token {
Token::LParen => {
let _consume_lparen_token = parser.next_token();
let start = Self::parse_string_or_number_or_word(
parser,
&[Token::Comma],
require_now_expr,
)?
.0;
let end = Self::parse_string_or_number_or_word(
parser,
&[Token::Comma],
require_now_expr,
)?
.0;
let exprs = parser
.parse_comma_separated(Parser::parse_expr)
.context(ParserSnafu)?;
let (step, delimiter) = Self::parse_string_or_number_or_word(
parser,
&[Token::Comma, Token::RParen],
false,
let param_count = exprs.len();
if param_count != 3 && param_count != 4 {
return Err(ParserError::ParserError(
format!("Expected 3 or 4 expressions in TQL parameters (start, end, step, [lookback]), but found {}", param_count)
))
.context(ParserSnafu);
}
let mut exprs_iter = exprs.into_iter();
// Safety: safe to call next and unwrap, because we already check the param_count above.
let start = Self::parse_expr_to_literal_or_ts(
exprs_iter.next().unwrap(),
require_now_expr,
)?;
let lookback = if delimiter == Token::Comma {
Self::parse_string_or_number_or_word(parser, &[Token::RParen], false)
.ok()
.map(|t| t.0)
} else {
None
};
let end = Self::parse_expr_to_literal_or_ts(
exprs_iter.next().unwrap(),
require_now_expr,
)?;
let step = Self::parse_expr_to_literal_or_ts(exprs_iter.next().unwrap(), false)?;
let lookback = exprs_iter
.next()
.map(|expr| Self::parse_expr_to_literal_or_ts(expr, false))
.transpose()?;
if !parser.consume_token(&Token::RParen) {
return Err(ParserError::ParserError(format!(
"Expected ')' after TQL parameters, but found: {}",
parser.peek_token()
)))
.context(ParserSnafu);
}
(start, end, step, lookback)
}
_ => ("0".to_string(), "0".to_string(), "5m".to_string(), None),
};
let query = Self::parse_tql_query(parser, self.sql).context(ParserSnafu)?;
Ok(TqlParameters::new(start, end, step, lookback, query))
}
@@ -179,72 +191,43 @@ impl ParserContext<'_> {
}
}
/// Try to parse and consume a string, number or word token.
/// Return `Ok` if it's parsed and one of the given delimiter tokens is consumed.
/// The string and matched delimiter will be returned as a tuple.
fn parse_string_or_number_or_word(
parser: &mut Parser,
delimiter_tokens: &[Token],
/// Parse the expression to a literal string or a timestamp in seconds.
fn parse_expr_to_literal_or_ts(
parser_expr: sqlparser::ast::Expr,
require_now_expr: bool,
) -> std::result::Result<(String, Token), TQLError> {
let mut tokens = vec![];
while !delimiter_tokens.contains(&parser.peek_token().token) {
let token = parser.next_token().token;
if matches!(token, Token::EOF) {
break;
}
tokens.push(token);
}
let result = match tokens.len() {
0 => Err(ParserError::ParserError(
"Expected at least one token".to_string(),
))
.context(ParserSnafu),
1 => {
let value = match tokens[0].clone() {
Token::Number(n, _) if !require_now_expr => n,
Token::DoubleQuotedString(s) | Token::SingleQuotedString(s)
if !require_now_expr =>
{
s
}
Token::Word(_) => Self::parse_tokens_to_ts(tokens, require_now_expr)?,
unexpected => {
if !require_now_expr {
return Err(ParserError::ParserError(format!(
"Expected number, string or word, but have {unexpected:?}"
)))
.context(ParserSnafu);
} else {
return Err(ParserError::ParserError(format!(
"Expected expression containing `now()`, but have {unexpected:?}"
)))
.context(ParserSnafu);
}
) -> std::result::Result<String, TQLError> {
match parser_expr {
sqlparser::ast::Expr::Value(v) => match v.value {
sqlparser::ast::Value::Number(s, _) if !require_now_expr => Ok(s),
sqlparser::ast::Value::DoubleQuotedString(s)
| sqlparser::ast::Value::SingleQuotedString(s)
if !require_now_expr =>
{
Ok(s)
}
unexpected => {
if !require_now_expr {
Err(ParserError::ParserError(format!(
"Expected number, string or word, but have {unexpected:?}"
)))
.context(ParserSnafu)
} else {
Err(ParserError::ParserError(format!(
"Expected expression containing `now()`, but have {unexpected:?}"
)))
.context(ParserSnafu)
}
};
Ok(value)
}
_ => Self::parse_tokens_to_ts(tokens, require_now_expr),
};
for token in delimiter_tokens {
if parser.consume_token(token) {
return result.map(|v| (v, token.clone()));
}
}
},
_ => Self::parse_expr_to_ts(parser_expr, require_now_expr),
}
Err(ParserError::ParserError(format!(
"Delimiters not match {delimiter_tokens:?}"
)))
.context(ParserSnafu)
}
/// Parse the tokens to seconds and convert to string.
fn parse_tokens_to_ts(
tokens: Vec<Token>,
/// Parse the expression to a timestamp in seconds.
fn parse_expr_to_ts(
parser_expr: sqlparser::ast::Expr,
require_now_expr: bool,
) -> std::result::Result<String, TQLError> {
let parser_expr = Self::parse_to_expr(tokens)?;
let lit = utils::parser_expr_to_scalar_value_literal(parser_expr, require_now_expr)
.map_err(Box::new)
.context(ConvertToLogicalExpressionSnafu)?;
@@ -267,13 +250,6 @@ impl ParserContext<'_> {
})
}
fn parse_to_expr(tokens: Vec<Token>) -> std::result::Result<sqlparser::ast::Expr, TQLError> {
Parser::new(&GreptimeDbDialect {})
.with_tokens(tokens)
.parse_expr()
.context(ParserSnafu)
}
fn parse_tql_query(parser: &mut Parser, sql: &str) -> std::result::Result<String, ParserError> {
while matches!(parser.peek_token().token, Token::Comma) {
let _skip_token = parser.next_token();
@@ -420,6 +396,60 @@ mod tests {
assert!(result.is_err());
}
#[test]
fn test_parse_tql_eval_with_date_trunc() {
let sql = "TQL EVAL (date_trunc('day', now() - interval '1' day), date_trunc('day', now()), '1h') http_requests_total{environment=~'staging|testing|development',method!='GET'} @ 1609746000 offset 5m";
let statement = parse_into_statement(sql);
match statement {
Statement::Tql(Tql::Eval(eval)) => {
// date_trunc('day', now() - interval '1' day) should resolve to start of yesterday
// date_trunc('day', now()) should resolve to start of today
// The exact values depend on when the test runs, but we can verify the structure
assert!(eval.start.parse::<i64>().is_ok());
assert!(eval.end.parse::<i64>().is_ok());
assert_eq!(eval.step, "1h");
assert_eq!(eval.lookback, None);
assert_eq!(
eval.query,
"http_requests_total{environment=~'staging|testing|development',method!='GET'} @ 1609746000 offset 5m"
);
}
_ => unreachable!(),
}
// Test with 4 parameters including lookback
let sql = "TQL EVAL (date_trunc('hour', now() - interval '6' hour), date_trunc('hour', now()), '30m', '5m') cpu_usage_total";
let statement = parse_into_statement(sql);
match statement {
Statement::Tql(Tql::Eval(eval)) => {
assert!(eval.start.parse::<i64>().is_ok());
assert!(eval.end.parse::<i64>().is_ok());
assert_eq!(eval.step, "30m");
assert_eq!(eval.lookback, Some("5m".to_string()));
assert_eq!(eval.query, "cpu_usage_total");
}
_ => unreachable!(),
}
}
#[test]
fn test_parse_tql_analyze_with_date_trunc() {
let sql = "TQL ANALYZE VERBOSE FORMAT JSON (date_trunc('week', now() - interval '2' week), date_trunc('week', now()), '4h', '1h') network_bytes_total";
let statement = parse_into_statement(sql);
match statement {
Statement::Tql(Tql::Analyze(analyze)) => {
assert!(analyze.start.parse::<i64>().is_ok());
assert!(analyze.end.parse::<i64>().is_ok());
assert_eq!(analyze.step, "4h");
assert_eq!(analyze.lookback, Some("1h".to_string()));
assert_eq!(analyze.query, "network_bytes_total");
assert!(analyze.is_verbose);
assert_eq!(analyze.format, Some(AnalyzeFormat::JSON));
}
_ => unreachable!(),
}
}
#[test]
fn test_parse_tql_eval() {
let sql = "TQL EVAL (1676887657, 1676887659, '1m') http_requests_total{environment=~'staging|testing|development',method!='GET'} @ 1609746000 offset 5m";
@@ -997,10 +1027,13 @@ mod tests {
let sql = "TQL EVAL (1676887657, 1676887659, 1m) http_requests_total{environment=~'staging|testing|development',method!='GET'} @ 1609746000 offset 5m";
let result =
ParserContext::create_with_dialect(sql, dialect, parse_options.clone()).unwrap_err();
assert!(
result
.output_msg()
.contains("Failed to extract a timestamp value")
.contains("Expected ')' after TQL parameters, but found: m"),
"{}",
result.output_msg()
);
// missing end
@@ -1010,7 +1043,9 @@ mod tests {
assert!(
result
.output_msg()
.contains("Failed to extract a timestamp value")
.contains("Expected 3 or 4 expressions in TQL parameters"),
"{}",
result.output_msg()
);
// empty TQL query
@@ -1023,6 +1058,12 @@ mod tests {
let sql = "tql eval (0, 0, '1s) t;;';";
let result =
ParserContext::create_with_dialect(sql, dialect, parse_options.clone()).unwrap_err();
assert!(result.output_msg().contains("Delimiters not match"));
assert!(
result
.output_msg()
.contains("Expected ')' after TQL parameters, but found: ;"),
"{}",
result.output_msg()
);
}
}
@@ -274,3 +274,95 @@ drop table test;
Affected Rows: 0
CREATE TABLE metrics(val DOUBLE, ts TIMESTAMP TIME INDEX, host STRING PRIMARY KEY);
Affected Rows: 0
-- Insert sample data with fixed timestamps for predictable testing
INSERT INTO metrics VALUES
(10.0, '2024-01-01 00:00:00', 'host1'),
(15.0, '2024-01-01 06:00:00', 'host1'),
(20.0, '2024-01-01 12:00:00', 'host1'),
(25.0, '2024-01-01 18:00:00', 'host1'),
(30.0, '2024-01-02 00:00:00', 'host1'),
(35.0, '2024-01-02 06:00:00', 'host1'),
(40.0, '2024-01-02 12:00:00', 'host1'),
(12.0, '2024-01-01 02:00:00', 'host2'),
(18.0, '2024-01-01 08:00:00', 'host2'),
(22.0, '2024-01-01 14:00:00', 'host2');
Affected Rows: 10
-- Test TQL with date_trunc function
-- SQLNESS SORT_RESULT 2 1
TQL EVAL (date_trunc('day', '2024-01-01 10:00:00'::timestamp),
date_trunc('day', '2024-01-02 10:00:00'::timestamp),
'6h') last_over_time(metrics[12h]);
+---------------------+-----------------------------------+-------+
| ts | prom_last_over_time(ts_range,val) | host |
+---------------------+-----------------------------------+-------+
| 2024-01-01T00:00:00 | 10.0 | host1 |
| 2024-01-01T06:00:00 | 12.0 | host2 |
| 2024-01-01T06:00:00 | 15.0 | host1 |
| 2024-01-01T12:00:00 | 18.0 | host2 |
| 2024-01-01T12:00:00 | 20.0 | host1 |
| 2024-01-01T18:00:00 | 22.0 | host2 |
| 2024-01-01T18:00:00 | 25.0 | host1 |
| 2024-01-02T00:00:00 | 22.0 | host2 |
| 2024-01-02T00:00:00 | 25.0 | host1 |
+---------------------+-----------------------------------+-------+
-- Test TQL with date_trunc and interval arithmetic (now() - interval '1' day pattern)
-- SQLNESS SORT_RESULT 2 1
TQL EVAL (date_trunc('day', '2024-01-02 00:00:00'::timestamp) - interval '1' day,
date_trunc('day', '2024-01-02 00:00:00'::timestamp),
'4h') metrics{host="host1"};
+------+---------------------+-------+
| val | ts | host |
+------+---------------------+-------+
| 10.0 | 2024-01-01T00:00:00 | host1 |
| 20.0 | 2024-01-01T12:00:00 | host1 |
+------+---------------------+-------+
-- Test TQL with hour-based queries
-- SQLNESS SORT_RESULT 2 1
TQL EVAL ('2024-01-01 06:00:00'::timestamp,
'2024-01-01 18:00:00'::timestamp,
'3h',
'6h') metrics;
+------+---------------------+-------+
| val | ts | host |
+------+---------------------+-------+
| 12.0 | 2024-01-01T06:00:00 | host2 |
| 15.0 | 2024-01-01T06:00:00 | host1 |
| 15.0 | 2024-01-01T09:00:00 | host1 |
| 18.0 | 2024-01-01T09:00:00 | host2 |
| 18.0 | 2024-01-01T12:00:00 | host2 |
| 20.0 | 2024-01-01T12:00:00 | host1 |
| 20.0 | 2024-01-01T15:00:00 | host1 |
| 22.0 | 2024-01-01T15:00:00 | host2 |
| 22.0 | 2024-01-01T18:00:00 | host2 |
| 25.0 | 2024-01-01T18:00:00 | host1 |
+------+---------------------+-------+
-- Test TQL with complex interval arithmetic
-- SQLNESS SORT_RESULT 2 1
TQL EVAL ('2024-01-01 00:00:00'::timestamp + interval '12' hour,
'2024-01-02 00:00:00'::timestamp - interval '6' hour,
'6h',
'6h') metrics{host="host2"};
+------+---------------------+-------+
| val | ts | host |
+------+---------------------+-------+
| 18.0 | 2024-01-01T12:00:00 | host2 |
| 22.0 | 2024-01-01T18:00:00 | host2 |
+------+---------------------+-------+
DROP TABLE metrics;
Affected Rows: 0
@@ -58,3 +58,46 @@ TQL EVAL (0, 10, '5s') test{__field__="Field_I"};
TQL EVAL (0, 10, '5s') test{__field__="field_i"};
drop table test;
CREATE TABLE metrics(val DOUBLE, ts TIMESTAMP TIME INDEX, host STRING PRIMARY KEY);
-- Insert sample data with fixed timestamps for predictable testing
INSERT INTO metrics VALUES
(10.0, '2024-01-01 00:00:00', 'host1'),
(15.0, '2024-01-01 06:00:00', 'host1'),
(20.0, '2024-01-01 12:00:00', 'host1'),
(25.0, '2024-01-01 18:00:00', 'host1'),
(30.0, '2024-01-02 00:00:00', 'host1'),
(35.0, '2024-01-02 06:00:00', 'host1'),
(40.0, '2024-01-02 12:00:00', 'host1'),
(12.0, '2024-01-01 02:00:00', 'host2'),
(18.0, '2024-01-01 08:00:00', 'host2'),
(22.0, '2024-01-01 14:00:00', 'host2');
-- Test TQL with date_trunc function
-- SQLNESS SORT_RESULT 2 1
TQL EVAL (date_trunc('day', '2024-01-01 10:00:00'::timestamp),
date_trunc('day', '2024-01-02 10:00:00'::timestamp),
'6h') last_over_time(metrics[12h]);
-- Test TQL with date_trunc and interval arithmetic (now() - interval '1' day pattern)
-- SQLNESS SORT_RESULT 2 1
TQL EVAL (date_trunc('day', '2024-01-02 00:00:00'::timestamp) - interval '1' day,
date_trunc('day', '2024-01-02 00:00:00'::timestamp),
'4h') metrics{host="host1"};
-- Test TQL with hour-based queries
-- SQLNESS SORT_RESULT 2 1
TQL EVAL ('2024-01-01 06:00:00'::timestamp,
'2024-01-01 18:00:00'::timestamp,
'3h',
'6h') metrics;
-- Test TQL with complex interval arithmetic
-- SQLNESS SORT_RESULT 2 1
TQL EVAL ('2024-01-01 00:00:00'::timestamp + interval '12' hour,
'2024-01-02 00:00:00'::timestamp - interval '6' hour,
'6h',
'6h') metrics{host="host2"};
DROP TABLE metrics;
@@ -62,7 +62,7 @@ SELECT * FROM tql;
+---------------------+-----------+
-- Hybrid CTEs (TQL + SQL)
WITH
WITH
tql_data (ts, val) AS (TQL EVAL (0, 40, '10s') metric),
filtered AS (SELECT * FROM tql_data WHERE val > 5)
SELECT count(*) FROM filtered;
@@ -74,7 +74,7 @@ SELECT count(*) FROM filtered;
+----------+
-- TQL CTE with complex PromQL expressions
WITH
WITH
tql_data (ts, val) AS (TQL EVAL (0, 40, '10s') rate(metric[20s])),
filtered (ts, val) AS (SELECT * FROM tql_data WHERE val > 0)
SELECT sum(val) FROM filtered;
@@ -126,13 +126,13 @@ SELECT host_metrics.ts, host_metrics.host FROM host_metrics;
+---------------------+-------+
-- Multiple TQL CTEs referencing different tables
WITH
WITH
metric_data(ts, val) AS (TQL EVAL (0, 40, '10s') metric),
label_data(ts, host, cpu) AS (TQL EVAL (0, 40, '10s') labels{host="host2"})
SELECT
SELECT
m.val as metric_val,
l.cpu as label_val
FROM metric_data m, label_data l
FROM metric_data m, label_data l
WHERE m.ts = l.ts
ORDER BY m.ts
LIMIT 3;
@@ -161,7 +161,7 @@ SELECT min(val) as min_computed, max(val) as max_computed FROM computed;
WITH tql_base(ts, val) AS (
TQL EVAL (0, 40, '10s') metric
)
SELECT
SELECT
ts,
val,
LAG(val, 1) OVER (ORDER BY ts) as prev_value
@@ -182,10 +182,10 @@ ORDER BY ts;
WITH tql_grouped(ts, host, cpu) AS (
TQL EVAL (0, 40, '10s') labels
)
SELECT
SELECT
DATE_TRUNC('minute', ts) as minute,
count(*) as point_count
FROM tql_grouped
FROM tql_grouped
GROUP BY minute
HAVING count(*) > 1;
@@ -197,7 +197,7 @@ HAVING count(*) > 1;
-- TQL CTE with UNION
-- SQLNESS SORT_RESULT 3 1
WITH
WITH
host1_data(ts, host, cpu) AS (TQL EVAL (0, 40, '10s') labels{host="host1"}),
host2_data(ts, host, cpu) AS (TQL EVAL (0, 40, '10s') labels{host="host2"})
SELECT 'host1' as source, ts, cpu FROM host1_data
@@ -220,11 +220,11 @@ SELECT 'host2' as source, ts, cpu FROM host2_data;
+--------+---------------------+-----+
-- Nested CTEs with TQL
WITH
WITH
base_tql(ts, val) AS (TQL EVAL (0, 40, '10s') metric),
processed(ts, percent) AS (
SELECT ts, val * 100 as percent
FROM base_tql
SELECT ts, val * 100 as percent
FROM base_tql
WHERE val > 0
),
final(ts, percent) AS (
@@ -259,7 +259,7 @@ SELECT * FROM time_shifted;
WITH tql_summary(ts, host, cpu) AS (
TQL EVAL (0, 40, '10s') avg_over_time(labels[30s])
)
SELECT
SELECT
t.ts,
t.cpu as avg_value,
l.host
@@ -287,7 +287,7 @@ SELECT * FROM tql_analyze limit 1;
Error: 2000(InvalidSyntax), Invalid SQL, error: Only TQL EVAL is supported in CTEs
-- Error case - TQL EXPLAIN should fail
-- Error case - TQL EXPLAIN should fail
WITH tql_explain AS (
TQL EXPLAIN (0, 40, '10s') metric
)
@@ -297,7 +297,7 @@ Error: 2000(InvalidSyntax), Invalid SQL, error: Only TQL EVAL is supported in CT
-- TQL CTE with lookback parameter
WITH tql_lookback AS (
TQL EVAL (0, 40, '10s', 15s) metric
TQL EVAL (0, 40, '10s', '15s') metric
)
SELECT count(*) FROM tql_lookback;
+15 -15
View File
@@ -33,13 +33,13 @@ WITH tql (the_timestamp, the_value) as (
SELECT * FROM tql;
-- Hybrid CTEs (TQL + SQL)
WITH
WITH
tql_data (ts, val) AS (TQL EVAL (0, 40, '10s') metric),
filtered AS (SELECT * FROM tql_data WHERE val > 5)
SELECT count(*) FROM filtered;
-- TQL CTE with complex PromQL expressions
WITH
WITH
tql_data (ts, val) AS (TQL EVAL (0, 40, '10s') rate(metric[20s])),
filtered (ts, val) AS (SELECT * FROM tql_data WHERE val > 0)
SELECT sum(val) FROM filtered;
@@ -63,13 +63,13 @@ WITH host_metrics AS (
SELECT host_metrics.ts, host_metrics.host FROM host_metrics;
-- Multiple TQL CTEs referencing different tables
WITH
WITH
metric_data(ts, val) AS (TQL EVAL (0, 40, '10s') metric),
label_data(ts, host, cpu) AS (TQL EVAL (0, 40, '10s') labels{host="host2"})
SELECT
SELECT
m.val as metric_val,
l.cpu as label_val
FROM metric_data m, label_data l
FROM metric_data m, label_data l
WHERE m.ts = l.ts
ORDER BY m.ts
LIMIT 3;
@@ -84,7 +84,7 @@ SELECT min(val) as min_computed, max(val) as max_computed FROM computed;
WITH tql_base(ts, val) AS (
TQL EVAL (0, 40, '10s') metric
)
SELECT
SELECT
ts,
val,
LAG(val, 1) OVER (ORDER BY ts) as prev_value
@@ -95,16 +95,16 @@ ORDER BY ts;
WITH tql_grouped(ts, host, cpu) AS (
TQL EVAL (0, 40, '10s') labels
)
SELECT
SELECT
DATE_TRUNC('minute', ts) as minute,
count(*) as point_count
FROM tql_grouped
FROM tql_grouped
GROUP BY minute
HAVING count(*) > 1;
-- TQL CTE with UNION
-- SQLNESS SORT_RESULT 3 1
WITH
WITH
host1_data(ts, host, cpu) AS (TQL EVAL (0, 40, '10s') labels{host="host1"}),
host2_data(ts, host, cpu) AS (TQL EVAL (0, 40, '10s') labels{host="host2"})
SELECT 'host1' as source, ts, cpu FROM host1_data
@@ -112,11 +112,11 @@ UNION ALL
SELECT 'host2' as source, ts, cpu FROM host2_data;
-- Nested CTEs with TQL
WITH
WITH
base_tql(ts, val) AS (TQL EVAL (0, 40, '10s') metric),
processed(ts, percent) AS (
SELECT ts, val * 100 as percent
FROM base_tql
SELECT ts, val * 100 as percent
FROM base_tql
WHERE val > 0
),
final(ts, percent) AS (
@@ -135,7 +135,7 @@ SELECT * FROM time_shifted;
WITH tql_summary(ts, host, cpu) AS (
TQL EVAL (0, 40, '10s') avg_over_time(labels[30s])
)
SELECT
SELECT
t.ts,
t.cpu as avg_value,
l.host
@@ -151,7 +151,7 @@ WITH tql_analyze AS (
)
SELECT * FROM tql_analyze limit 1;
-- Error case - TQL EXPLAIN should fail
-- Error case - TQL EXPLAIN should fail
WITH tql_explain AS (
TQL EXPLAIN (0, 40, '10s') metric
)
@@ -159,7 +159,7 @@ SELECT * FROM tql_explain limit 1;
-- TQL CTE with lookback parameter
WITH tql_lookback AS (
TQL EVAL (0, 40, '10s', 15s) metric
TQL EVAL (0, 40, '10s', '15s') metric
)
SELECT count(*) FROM tql_lookback;