Add quote-to-bracket escaping for calculated measures with multiple ad-hoc measures - #50
Conversation
This ensures that extend operations (which may reference summarized columns) are executed after the summarize clause in the generated KQL query.
…d hoc measures - Add _find_operator_outside_quotes() helper to find operators not inside quoted strings - Update _escape_and_quote_columns() to recursively escape both sides of operators - Update _is_number_literal() to match integers and decimals with digits on both sides - Add tests for: - Two quoted measures: "Measure 1" + "Measure 2" -> ["Measure 1"] + ["Measure 2"] - Measure with constant: "Measure 1" * 2 -> ["Measure 1"] * 2 - Measure with operator in name: "Measure 1-2" -> ["Measure 1-2"] - _is_number_literal function validation
There was a problem hiding this comment.
Pull request overview
This PR enhances the Kusto KQL SQLAlchemy compiler to correctly escape/convert calculated measure expressions that combine multiple quoted measure references and arithmetic (e.g., "Measure 1" + "Measure 2", "Measure 1" * 2) into the dialect’s bracket-escaped form (e.g., ["Measure 1"] + ["Measure 2"]). It also updates query compilation ordering to ensure | summarize precedes | extend, which is required for calculated measures referencing summarized outputs (noted as dependent on PR #48).
Changes:
- Add
_find_operator_outside_quotes()and update_escape_and_quote_columns()to recursively escape both sides of arithmetic operators outside quotes. - Update
_is_number_literal()to recognize decimals with digits on both sides of the decimal point. - Expand/adjust unit tests for calculated measures and update expectations for
summarize/extendordering.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
sqlalchemy_kusto/dialect_kql.py |
Implements recursive escaping for arithmetic expressions and enforces summarize before extend in compiled KQL. |
tests/unit/test_dialect_kql.py |
Updates expected compiled KQL ordering and adds coverage for multi-measure calculated expressions and numeric literal detection. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Add summarize first if it exists | ||
| if "summarize" in projections_parts_dict: | ||
| compiled_query_lines.append(projections_parts_dict.pop("summarize")) | ||
|
|
||
| # Then add extend after summarize | ||
| if "extend" in projections_parts_dict: | ||
| compiled_query_lines.append(projections_parts_dict.pop("extend")) | ||
|
|
||
| # Add remaining parts (project, sort) | ||
| for statement_part in projections_parts_dict.values(): | ||
| if statement_part: | ||
| compiled_query_lines.append(statement_part) |
There was a problem hiding this comment.
projections_parts_dict always contains the keys summarize and extend (returned unconditionally by _get_projection_or_summarize), so the current if "summarize" in projections_parts_dict: / if "extend" in projections_parts_dict: checks are always true and will pop/append empty strings. Consider checking the value (projections_parts_dict.get("summarize")) or explicitly appending summarize/extend only when non-empty to avoid confusing control flow.
| # Add summarize first if it exists | ||
| if "summarize" in projections_parts_dict: | ||
| compiled_query_lines.append(projections_parts_dict.pop("summarize")) | ||
|
|
||
| # Then add extend after summarize | ||
| if "extend" in projections_parts_dict: | ||
| compiled_query_lines.append(projections_parts_dict.pop("extend")) | ||
|
|
||
| # Add remaining parts (project, sort) | ||
| for statement_part in projections_parts_dict.values(): |
There was a problem hiding this comment.
The PR description indicates this should be merged after PR #48, but this diff still includes the summarize/extend reordering change. To keep this PR focused on quote-to-bracket escaping (and reduce merge conflicts), please rebase onto the branch that includes #48 so those unrelated changes drop out of this PR.
There was a problem hiding this comment.
Will do once the other PR merges in
…ntermediary measures, and parentheses support for calculated measures
Merge After This PR: #48
Summary
This PR enhances the KQL dialect to properly handle calculated measures that contain multiple quoted column references combined with arithmetic operators or constants.
Problem
Previously, expressions like "Measure 1" + "Measure 2" or "Measure 1" * 2 were not properly escaped. The quote-stripping logic would incorrectly process these expressions, breaking the KQL output. Also, measures containing operators in their names were incorrectly parsed
Technical Details
_find_operator_outside_quotes()added to find operators not part of the measure name_escape_and_quote_columns()recursively on the left and right side of the operator. This ensures all parts of the expression get bracketed and quoted. Exclude numbers from getting quoted_is_number_literal()to include decimals that start and end with a numberUI Changes
Before:


After: