mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
fix(sl): parse user filter expressions as predicates, not projections (#307)
* fix(sl): parse user filter expressions as predicates, not projections
User-authored filters and segments were parsed in a projection context
(`SELECT {expr}`). On T-SQL a top-level `col = 'value'` projection is the
`alias = expression` aliasing syntax, so an equality filter parsed this way
became `'value' AS col` — dropping the comparison entirely and silently
skipping computed-column expansion (the column hid behind the alias).
Parse user fragments as predicates (`SELECT * WHERE {expr}`) at every parse
site — the parser cache, measure-filter CASE WHEN generation, computed-column
expansion, and measure-filter/segment column qualification. For plain
non-condition expressions the column set is identical, so this is a no-op
everywhere except the T-SQL alias case it fixes.
Add cross-dialect regression tests (tsql, postgres, snowflake, bigquery)
locking equality filters/segments to comparison shape and confirming `= 'x'`
now matches `IN ('x')` on T-SQL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Shorten T-SQL predicate comments
* docs(sl): tighten T-SQL predicate docstrings and AGENTS docstring rule
Trim the parser and regression-test docstrings to the 1-3 line bar and
extend the AGENTS.md comment guidance to cover docstrings explicitly.
* refactor(sl): route all filter parsing through parse_predicate
Consolidate the predicate-context parse into a single parse_predicate
helper and route every filter-parsing call site through it: measure
CASE-WHEN filters, segments, computed-column-in-filter, the
aggregate-locality HAVING rewrite, and the planner OR-mixing /
top-level-AND split. The locality and split paths still parsed user
filters in projection context, so a named-measure equality filter
compiled to `0 AS measure` on T-SQL. Add a locality regression test
covering the HAVING rewrite path.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Andrey Avtomonov <andreybavt@gmail.com>
This commit is contained in:
parent
4dae8c34dd
commit
fb50c11d16
5 changed files with 238 additions and 63 deletions
|
|
@ -15,7 +15,11 @@ from semantic_layer.models import (
|
|||
ResolvedPlan,
|
||||
SourceDefinition,
|
||||
)
|
||||
from semantic_layer.parser import ExpressionParser, quote_reserved_identifiers
|
||||
from semantic_layer.parser import (
|
||||
ExpressionParser,
|
||||
parse_predicate,
|
||||
quote_reserved_identifiers,
|
||||
)
|
||||
|
||||
# DIALECT CONVENTION:
|
||||
# User-authored SQL fragments (measure `expr`, segment `expr`, filter,
|
||||
|
|
@ -673,9 +677,7 @@ class SqlGenerator:
|
|||
if isinstance(select_expr, exp.Alias):
|
||||
select_expr = select_expr.this
|
||||
|
||||
filter_cond = sqlglot.parse_one(
|
||||
f"SELECT {filter_sql}", read=self.dialect
|
||||
).expressions[0]
|
||||
filter_cond = parse_predicate(filter_sql, self.dialect)
|
||||
|
||||
def _make_case(inner_node):
|
||||
return exp.Case(
|
||||
|
|
@ -1073,10 +1075,7 @@ class SqlGenerator:
|
|||
|
||||
# AST-based rewriting for robustness
|
||||
try:
|
||||
tree = sqlglot.parse_one(
|
||||
f"SELECT {quote_reserved_identifiers(filter_expr)}",
|
||||
read=self.dialect,
|
||||
)
|
||||
condition = parse_predicate(filter_expr, self.dialect)
|
||||
|
||||
def _rewrite(node):
|
||||
if isinstance(node, (exp.AggFunc, exp.Anonymous)):
|
||||
|
|
@ -1099,8 +1098,8 @@ class SqlGenerator:
|
|||
).expressions[0]
|
||||
return node
|
||||
|
||||
transformed = tree.transform(_rewrite)
|
||||
return transformed.expressions[0].sql(dialect=self.dialect)
|
||||
transformed = condition.transform(_rewrite)
|
||||
return transformed.sql(dialect=self.dialect)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"AST-based HAVING rewrite failed for locality filter, falling back to regex: %s",
|
||||
|
|
@ -1292,9 +1291,7 @@ class SqlGenerator:
|
|||
return expr
|
||||
|
||||
try:
|
||||
tree = sqlglot.parse_one(
|
||||
f"SELECT {quote_reserved_identifiers(expr)}", read=self.dialect
|
||||
)
|
||||
condition = parse_predicate(expr, self.dialect)
|
||||
|
||||
changed = False
|
||||
|
||||
|
|
@ -1309,9 +1306,9 @@ class SqlGenerator:
|
|||
).expressions[0]
|
||||
return node
|
||||
|
||||
transformed = tree.transform(_replace)
|
||||
transformed = condition.transform(_replace)
|
||||
if changed:
|
||||
return transformed.expressions[0].sql(dialect=self.dialect)
|
||||
return transformed.sql(dialect=self.dialect)
|
||||
except Exception:
|
||||
logger.debug("AST-based computed column expansion failed for: %s", expr)
|
||||
|
||||
|
|
@ -1357,13 +1354,7 @@ class SqlGenerator:
|
|||
|
||||
# Use AST to find and replace column references matching measure names
|
||||
try:
|
||||
tree = sqlglot.parse_one(
|
||||
f"SELECT * WHERE {quote_reserved_identifiers(f)}",
|
||||
dialect=self.dialect,
|
||||
)
|
||||
where = tree.find(exp.Where)
|
||||
if not where:
|
||||
return f
|
||||
condition = parse_predicate(f, self.dialect)
|
||||
|
||||
changed = False
|
||||
|
||||
|
|
@ -1388,7 +1379,7 @@ class SqlGenerator:
|
|||
).expressions[0]
|
||||
return node
|
||||
|
||||
new_where = where.this.transform(_replace)
|
||||
new_where = condition.transform(_replace)
|
||||
if changed:
|
||||
return new_where.sql(dialect=self.dialect)
|
||||
except Exception:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue