Make stats DSL primary with highlighted editor, completion and live diagnostics

This commit is contained in:
Peter Stockings
2026-10-02 16:15:38 +10:00
parent f951e888bd
commit 987f470ea5
14 changed files with 11534 additions and 105 deletions
+122 -1
View File
@@ -6,7 +6,8 @@ from test_app import add_history, login
from app.extensions import db
from app.models import Activity
from app.stats import explore, parse_query, query_text
from app.stats import explore, parse_query, period_bounds, query_text
from app.stats_dsl import QueryError
pytest_plugins = ("test_app",)
@@ -164,3 +165,123 @@ def test_evidence_pagination(app):
assert len(second.json["rows"]) == 6
assert not {r["id"] for r in response.json["rows"]} & {r["id"] for r in second.json["rows"]}
assert client.get("/stats/runs", query_string={**args, "page": 3}).status_code == 400
def test_expanded_dsl_roundtrip_units_comments_and_dates():
source = """# My long runs
runs
during 2026-07-01 to 2026-09-30
where distance between 8000 and 20000 m
and pace <= 5:30 /km
and duration >= 45 min
and has cadence
and missing power
and title contains "River"
summarize pace, heart_rate, cadence, duration
by day
sort by pace asc
compare none"""
query = parse_query({"query": source})
assert query["extra_metrics"] == ["cadence", "duration"]
assert query["group"] == "day"
assert query["minimum"] == 8
assert query["maximum"] == 20
assert query["filters"][0]["value"] == 330
assert query["filters"][1]["value"] == 0.75
assert parse_query({"query": query_text(query)}) == query
months = parse_query({"query": "runs\nduring last 1 month\nending 2026-03-30\nsummarize pace"})
assert period_bounds(months, "Australia/Sydney")[0].isoformat() == "2026-02-28"
days = parse_query({"query": "runs\nduring last 30 days\nsummarize distance"})
assert parse_query({"query": query_text(days)}) == days
@pytest.mark.parametrize(
("clause", "message"),
[
("and pace <= 5:99 /km", "seconds"),
("and pace <= 5:30 bpm", "Use /km"),
("and heart_rate between 150 and 140 bpm", "lower bound"),
("and has heart_rae", "heart_rate"),
("during last 25 months", "1–24"),
("summarize pace, pace", "different metrics"),
("sort by owner_id asc", "Unknown metric"),
],
)
def test_source_located_diagnostics(clause, message):
source = "runs\n" + clause
if not clause.startswith("summarize"):
source += "\nsummarize pace"
with pytest.raises(QueryError, match=message) as error:
parse_query({"query": source})
assert error.value.line == 2
def test_live_validation_and_extended_execution(app):
client = app.test_client()
assert client.post("/stats/validate", json={"query": "runs"}).status_code == 302
login(client)
assert client.post("/stats/validate", json={"query": "runs"}).json["valid"] is False
bad = client.post(
"/stats/validate", json={"query": "runs\nand pace <= 5:99 /km\nsummarize pace"}
)
assert bad.json["diagnostics"][0]["line"] == 2
source = """runs
during 2026-09-01 to 2026-10-02
where distance between 4 and 6 km
and pace >= 5:30 /km
and duration >= 25 min
and title contains "QUIET"
and missing cadence
summarize pace, heart_rate, distance, duration
by day
compare none
sort by distance desc"""
validation = client.post("/stats/validate", json={"query": source})
assert validation.json["valid"] is True
response = client.get("/stats", query_string={"query": source})
assert response.status_code == 200
assert b"A quiet morning run" in response.data
with app.app_context():
result = explore(1, "Australia/Sydney", parse_query({"query": source}))
assert result["count"] == 1
assert len(result["cards"]) == 4
assert client.post("/stats/validate", json={"query": 123}).status_code == 400
assert client.post("/stats/validate", json={"query": "a" * 4001}).json["valid"] is False
def test_validation_requires_csrf_and_does_not_read_activities(app):
client = app.test_client()
login(client)
with app.app_context():
statements = []
def record(_conn, _cursor, statement, _parameters, _context, _many):
statements.append(statement)
event.listen(db.engine, "before_cursor_execute", record)
engine = db.engine
try:
response = client.post("/stats/validate", json={"query": "runs\nsummarize pace"})
assert response.json["valid"] is True
assert not any("FROM activity" in statement for statement in statements)
finally:
event.remove(engine, "before_cursor_execute", record)
app.config["WTF_CSRF_ENABLED"] = True
assert client.post("/stats/validate", json={"query": "runs\nsummarize pace"}).status_code == 400
def test_fractional_units_roundtrip_and_strict_sorting(app):
query = parse_query(
{"query": "runs\nwhere duration >= 1 min\nand distance > 0.123456789 km\nsummarize pace"}
)
assert parse_query({"query": query_text(query)}) == query
with app.app_context():
add_history(3)
db.session.commit()
query = parse_query(
{"query": "runs\nduring 2026-09-01 to 2026-10-02\nsummarize pace\nsort by pace asc"}
)
rows = explore(1, "Australia/Sydney", query)["rows"]
assert rows[-1]["id"] == 1
query["filters"] = [{"field": "pace", "op": "<", "value": 300}]
assert explore(1, "Australia/Sydney", query)["count"] == 0