Skip to content

Commit f8a50b5

Browse files
zzylolclaude
andcommitted
feat(asap-tools): take ASAPQuery SQL from the table's structured entries
sketch-bench's exporter now writes each structured e2e entry's ASAPQuery SQL (`sql`, e.g. `... WHERE ts BETWEEN DATEADD(s, -300, NOW()) AND NOW() ...`), so it is the single ASAPQuery SQL renderer. asapquery.sql now takes that SQL as written and only replaces NOW() with the floored eval time; this module's own dialect renderer is gone. The ClickHouse raw SQL still comes from the structured fields. The frontend's DATEADD takes only NOW(), a literal or a CAST as its base, so NOW() becomes a datetime literal floored in the client's Jinja template: {{ end_datetime }} (UTC) with its seconds floored for T dividing 60, or its minutes for 60 | T | 3600. Other intervals raise. All 936 distinct structured queries in sketch-bench cd51be9's tables (T = 60 s and 300 s), rendered through the client's QueryTemplate at three end times, parse in ASAPQuery's SQL frontend to (start, duration) = (floor(E) - W, W) and pass its matcher. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
1 parent c0be5c5 commit f8a50b5

2 files changed

Lines changed: 71 additions & 28 deletions

File tree

‎asap-tools/experiments/datasets/fake_metric/sketch_bench_workloads.py‎

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
For a table `<stem>.json` this writes, under OUT_CONFIG_DIR:
1212
- <stem>/init.sql: the metric tables;
1313
- <stem>/interval_<T>s.sql: one statement per query that repeats every T s;
14-
- <stem>/asapquery.sql: the structured queries in ASAPQuery's SQL dialect;
14+
- <stem>/asapquery.sql: each structured entry's `sql` (ASAPQuery's SQL, as
15+
sketch-bench's exporter writes it) with NOW() replaced by the floored eval
16+
time;
1517
- <stem>_clickhouse.yaml: a baseline-only experiment_run_clickhouse.py config
1618
with one query group per T, repeating every T seconds.
1719
@@ -310,30 +312,37 @@ def raw_sql(q: Query, labels: Dict[str, List[str]]) -> str:
310312
)
311313

312314

313-
def asapquery_sql(q: Query) -> str:
314-
"""A structured query in the dialect of ASAPQuery's SQL frontend.
315+
def floored_end_datetime(interval_secs: int) -> str:
316+
"""A Jinja template for the client's {{ end_datetime }} floored to a
317+
multiple of the interval T, as the ClickHouse SQL floors {{ end_time }}.
315318
316-
The frontend takes a datetime literal but cannot floor it, so the window
317-
ends `{{ end_time }} % T` seconds before the client's {{ end_datetime }}
318-
(UTC; the frontend reads timezone-naive literals as server-local time):
319-
the same floored T as the ClickHouse SQL. The frontend maps the BETWEEN to
320-
(start, duration) = (T - W, W) and answers over (T - W, T], as the
321-
ClickHouse SQL does for structured queries.
319+
end_datetime is UTC 'YYYY-MM-DD HH:MM:SS', and UTC timestamps are
320+
multiples of 60 exactly at whole minutes, so flooring rewrites the seconds
321+
(T divides 60) or the minutes (T divides 3600 and is a multiple of 60).
322322
"""
323-
group = ", ".join(q.grouping)
324-
if q.agg == "cardinality":
325-
agg = f"COUNT(DISTINCT {q.value})"
326-
elif q.agg == "quantile":
327-
agg = f"QUANTILE({q.quantile}, {q.value})"
328-
else:
329-
raise ValueError(f"no ASAPQuery SQL for {q}")
330-
t = q.interval_secs
331-
end = "'{{ end_datetime }}'"
332-
return (
333-
f"SELECT {group}, {agg} FROM {q.table} WHERE ts BETWEEN "
334-
f"DATEADD(s, -{{{{ end_time % {t} + {q.window_secs} }}}}, {end}) AND "
335-
f"DATEADD(s, -{{{{ end_time % {t} }}}}, {end}) GROUP BY {group}"
336-
)
323+
t = interval_secs
324+
if 60 % t == 0:
325+
floored = f"end_time % 60 // {t} * {t}"
326+
return f'{{{{ end_datetime[:17] }}}}{{{{ "%02d" | format({floored}) }}}}'
327+
if t % 60 == 0 and 3600 % t == 0:
328+
floored = f"end_time % 3600 // {t} * {t} // 60"
329+
return f'{{{{ end_datetime[:14] }}}}{{{{ "%02d" | format({floored}) }}}}:00'
330+
raise ValueError(f"cannot floor a datetime to {t}s: T must divide 60 or 3600")
331+
332+
333+
def asapquery_sql(entry: dict) -> str:
334+
"""A structured entry's `sql` with NOW() replaced by the floored eval time.
335+
336+
ASAPQuery's SQL frontend takes a datetime literal but cannot floor it, so
337+
the literal is floored in the client's template (UTC; the frontend reads
338+
timezone-naive literals as server-local time). The frontend maps
339+
`ts BETWEEN DATEADD(s, -W, E) AND E` to (start, duration) = (E - W, W) and
340+
answers over (E - W, E], as the ClickHouse SQL does for structured queries.
341+
"""
342+
if "sql" not in entry:
343+
raise ValueError(f"structured entry without its ASAPQuery sql: {entry}")
344+
end = floored_end_datetime(entry["interval_secs"])
345+
return entry["sql"].replace("NOW()", f"'{end}'")
337346

338347

339348
def value_columns(desc: dict) -> List[str]:
@@ -393,7 +402,7 @@ def convert(
393402
f"-- {describe(entry)}\n{raw_sql(q, labels)}"
394403
)
395404
if "promql" not in entry:
396-
asapquery.append(f"-- {describe(entry)}\n{asapquery_sql(q)}")
405+
asapquery.append(f"-- {describe(entry)}\n{asapquery_sql(entry)}")
397406
if asapquery:
398407
# For the ASAPQuery arm; the runner does not read it.
399408
write_statements(os.path.join(sql_dir, "asapquery.sql"), asapquery)

‎asap-tools/experiments/tests/test_sketch_bench_workloads.py‎

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
CLICKHOUSE_BINARY) and is skipped otherwise.
77
"""
88

9+
import datetime
910
import itertools
1011
import json
1112
import math
@@ -18,6 +19,7 @@
1819
import unittest
1920
from typing import Callable, Dict, List, Tuple
2021

22+
import jinja2
2123
import yaml
2224

2325
sys.path.insert(
@@ -164,11 +166,43 @@ def mg_entry(capability: str, grouping: List[str], window_secs: int) -> dict:
164166
"interval_secs": 60,
165167
"rqes": [],
166168
}
169+
agg = "COUNT(DISTINCT user_id)"
167170
if capability == "quantile":
168171
entry["quantile"] = 0.99
172+
agg = "QUANTILE(0.99, latency)"
173+
group = ", ".join(grouping)
174+
entry["sql"] = (
175+
f"SELECT {group}, {agg} FROM http WHERE ts BETWEEN "
176+
f"DATEADD(s, -{window_secs}, NOW()) AND NOW() GROUP BY {group}"
177+
)
169178
return entry
170179

171180

181+
class FlooredEndDatetimeTest(unittest.TestCase):
182+
def test_renders_floored_utc_datetime(self):
183+
env = jinja2.Environment()
184+
for t in (1, 15, 60, 300, 3600):
185+
template = env.from_string(sbw.floored_end_datetime(t))
186+
for end in (1767229237, 1767229200, 1767225599, 1767232799):
187+
with self.subTest(t=t, end=end):
188+
# As the client's TimeRange.end_datetime formats it.
189+
got = template.render(
190+
end_time=end,
191+
end_datetime=datetime.datetime.fromtimestamp(
192+
end, tz=datetime.timezone.utc
193+
).strftime("%Y-%m-%d %H:%M:%S"),
194+
)
195+
want = datetime.datetime.fromtimestamp(
196+
end // t * t, tz=datetime.timezone.utc
197+
)
198+
self.assertEqual(got, want.strftime("%Y-%m-%d %H:%M:%S"))
199+
200+
def test_unfloorable_interval_raises(self):
201+
for t in (7, 420, 7200):
202+
with self.subTest(t=t), self.assertRaises(ValueError):
203+
sbw.floored_end_datetime(t)
204+
205+
172206
class ConvertTest(unittest.TestCase):
173207
def test_writes_config_grouped_by_interval(self):
174208
e2e = {
@@ -215,14 +249,14 @@ def test_writes_config_grouped_by_interval(self):
215249
init,
216250
)
217251
sql_dir = os.path.dirname(g60["sql_file"]["baseline"])
252+
# The table's sql as written, with only NOW() substituted.
253+
end = "'" + sbw.floored_end_datetime(60) + "'"
218254
with open(os.path.join(sql_dir, "asapquery.sql")) as f:
219255
self.assertEqual(
220256
f.read(),
221257
"-- quantile(0.99, latency) by (service) over 300s\n"
222-
"SELECT service, QUANTILE(0.99, latency) FROM http WHERE ts "
223-
"BETWEEN DATEADD(s, -{{ end_time % 60 + 300 }}, "
224-
"'{{ end_datetime }}') AND DATEADD(s, -{{ end_time % 60 }}, "
225-
"'{{ end_datetime }}') GROUP BY service;\n",
258+
+ e2e["queries"][-1]["sql"].replace("NOW()", end)
259+
+ ";\n",
226260
)
227261
finally:
228262
os.chdir(cwd)

0 commit comments

Comments
 (0)