Skip to content

Commit 00c1909

Browse files
committed
Restrict concat rewrite to VARCHAR, hide untranslatable fields
- _convert_string_concat no longer matches + CAST(...) generically; only AS VARCHAR) + triggers the rewrite, preserving INT/FLOAT arithmetic. - Untranslatable formulas (LOD, table calcs) now set public=False and sql='NULL' to prevent raw Tableau syntax from being emitted in generated SQL queries.
1 parent f3d4c2e commit 00c1909

3 files changed

Lines changed: 25 additions & 5 deletions

File tree

sidemantic/adapters/tableau.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,8 @@ def _convert_string_concat(text: str) -> str:
418418
"""Convert Tableau's + string concatenation to SQL ||.
419419
420420
Replaces + with || when at least one adjacent operand is a string-producing
421-
expression: a string literal ('...') or a CAST(... AS VARCHAR).
421+
expression: a string literal ('...') or a CAST(... AS VARCHAR) result.
422+
Only matches VARCHAR casts (not INTEGER/DOUBLE) to avoid breaking arithmetic.
422423
"""
423424
result = text
424425
prev = None
@@ -427,9 +428,8 @@ def _convert_string_concat(text: str) -> str:
427428
# 'string' + ... or ... + 'string'
428429
result = re.sub(r"('\s*)\+(\s*)", r"\1||\2", result)
429430
result = re.sub(r"(\s*)\+(\s*')", r"\1||\2", result)
430-
# CAST(... AS VARCHAR) + ... or ... + CAST(... AS VARCHAR)
431+
# CAST(... AS VARCHAR) + ... (only VARCHAR, not INTEGER/DOUBLE)
431432
result = re.sub(r"(AS\s+VARCHAR\)\s*)\+(\s*)", r"\1||\2", result, flags=re.IGNORECASE)
432-
result = re.sub(r"(\s*)\+(\s*CAST\()", r"\1||\2", result, flags=re.IGNORECASE)
433433
return result
434434

435435

@@ -994,6 +994,12 @@ def _parse_column(
994994
if not is_translatable:
995995
metadata = {"tableau_formula": formula}
996996

997+
# Untranslatable formulas (LOD, table calcs) produce non-queryable fields
998+
# with NULL sql to prevent raw Tableau syntax in generated SQL
999+
if not is_translatable:
1000+
hidden = True
1001+
sql_expr = "NULL"
1002+
9971003
if role == "measure":
9981004
return self._build_metric(
9991005
col_name, aggregation, sql_expr, caption, hidden, is_translatable, formula, metadata
@@ -1056,10 +1062,13 @@ def _build_metric(
10561062

10571063
if agg_lower in _PASSTHROUGH_AGGS or not is_translatable:
10581064
# Passthrough or untranslatable: make derived metric
1065+
# Use NULL placeholder for untranslatable formulas to prevent
1066+
# raw Tableau syntax from being emitted in SQL queries
1067+
safe_sql = "NULL" if not is_translatable else (sql or name)
10591068
return Metric(
10601069
name=name,
10611070
type="derived",
1062-
sql=sql or name,
1071+
sql=safe_sql,
10631072
label=caption,
10641073
public=not hidden,
10651074
metadata=metadata,

tests/adapters/tableau/test_formula.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,14 @@ def test_arithmetic_plus_preserved():
284284
assert "||" not in sql
285285

286286

287+
def test_numeric_cast_plus_preserved():
288+
"""INT() + FLOAT() keeps arithmetic +, not ||."""
289+
sql, ok = _translate_formula("[x] + INT([y])")
290+
assert ok
291+
assert "+" in sql
292+
assert "||" not in sql
293+
294+
287295
def test_dateadd():
288296
"""DATEADD('unit', n, date) -> date_add(date, INTERVAL (n) unit)."""
289297
sql, ok = _translate_formula("DATEADD('hour', 3, [created_at])")

tests/adapters/tableau/test_parsing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def test_aggregation_mapping_all_aggs(adapter):
188188

189189

190190
def test_lod_expression_preserved(adapter):
191-
"""LOD expressions are not translated; raw formula stored in metadata."""
191+
"""LOD expressions are not translated; raw formula stored in metadata, hidden from queries."""
192192
graph = adapter.parse(FIXTURES / "kitchen_sink.tds")
193193
model = graph.models["kitchen_sink"]
194194

@@ -198,6 +198,9 @@ def test_lod_expression_preserved(adapter):
198198
assert lod.metadata is not None
199199
assert "tableau_formula" in lod.metadata
200200
assert "{FIXED" in lod.metadata["tableau_formula"]
201+
# Untranslatable formulas should be hidden and use safe SQL
202+
assert lod.public is False
203+
assert lod.sql == "NULL"
201204

202205

203206
# =============================================================================

0 commit comments

Comments
 (0)