Skip to content

Commit aa97d2f

Browse files
fix(pinot): dialect date truncation (#35420)
Co-authored-by: bito-code-review[bot] <188872107+bito-code-review[bot]@users.noreply.github.com>
1 parent 28389de commit aa97d2f

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

superset/sql/dialects/pinot.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ class Generator(MySQL.Generator):
7878
exp.DataType.Type.UBIGINT: "UNSIGNED",
7979
}
8080

81+
TRANSFORMS = {
82+
**MySQL.Generator.TRANSFORMS,
83+
}
84+
# Remove DATE_TRUNC transformation - Pinot supports standard SQL DATE_TRUNC
85+
TRANSFORMS.pop(exp.DateTrunc, None)
86+
8187
def datatype_sql(self, expression: exp.DataType) -> str:
8288
# Don't use MySQL's VARCHAR size requirement logic
8389
# Just use TYPE_MAPPING for all types
@@ -95,3 +101,8 @@ def datatype_sql(self, expression: exp.DataType) -> str:
95101
return f"{type_sql} UNSIGNED{nested}"
96102

97103
return f"{type_sql}{nested}"
104+
105+
def cast_sql(self, expression: exp.Cast, safe_prefix: str | None = None) -> str:
106+
# Pinot doesn't support MySQL's TIMESTAMP() function
107+
# Use standard CAST syntax instead
108+
return super(MySQL.Generator, self).cast_sql(expression, safe_prefix)

tests/unit_tests/sql/dialects/pinot_tests.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,3 +421,80 @@ def test_unsigned_type() -> None:
421421

422422
assert "UNSIGNED" in result
423423
assert "BIGINT" in result
424+
425+
426+
def test_date_trunc_preserved() -> None:
427+
"""
428+
Test that DATE_TRUNC is preserved and not converted to MySQL's DATE() function.
429+
"""
430+
sql = "SELECT DATE_TRUNC('day', dt_column) FROM table"
431+
result = sqlglot.parse_one(sql, Pinot).sql(Pinot)
432+
433+
assert "DATE_TRUNC" in result
434+
assert "date_trunc('day'" in result.lower()
435+
# Should not be converted to MySQL's DATE() function
436+
assert result != "SELECT DATE(dt_column) FROM table"
437+
438+
439+
def test_cast_timestamp_preserved() -> None:
440+
"""
441+
Test that CAST AS TIMESTAMP is preserved and not converted to TIMESTAMP() function.
442+
"""
443+
sql = "SELECT CAST(dt_column AS TIMESTAMP) FROM table"
444+
result = sqlglot.parse_one(sql, Pinot).sql(Pinot)
445+
446+
assert "CAST" in result
447+
assert "AS TIMESTAMP" in result
448+
# Should not be converted to MySQL's TIMESTAMP() function
449+
assert "TIMESTAMP(dt_column)" not in result
450+
451+
452+
def test_date_trunc_with_cast_timestamp() -> None:
453+
"""
454+
Test the original complex query with DATE_TRUNC and CAST AS TIMESTAMP.
455+
Verifies that both are preserved in parse/generate round-trip.
456+
"""
457+
sql = """
458+
SELECT
459+
CAST(
460+
DATE_TRUNC(
461+
'day',
462+
CAST(
463+
DATETIMECONVERT(
464+
dt_epoch_ms, '1:MILLISECONDS:EPOCH',
465+
'1:MILLISECONDS:EPOCH', '1:MILLISECONDS'
466+
) AS TIMESTAMP
467+
)
468+
) AS TIMESTAMP
469+
),
470+
SUM(a) + SUM(b)
471+
FROM
472+
"default".c
473+
WHERE
474+
dt_epoch_ms >= 1735690800000
475+
AND dt_epoch_ms < 1759328588000
476+
AND locality != 'US'
477+
GROUP BY
478+
CAST(
479+
DATE_TRUNC(
480+
'day',
481+
CAST(
482+
DATETIMECONVERT(
483+
dt_epoch_ms, '1:MILLISECONDS:EPOCH',
484+
'1:MILLISECONDS:EPOCH', '1:MILLISECONDS'
485+
) AS TIMESTAMP
486+
)
487+
) AS TIMESTAMP
488+
)
489+
LIMIT
490+
10000
491+
"""
492+
result = sqlglot.parse_one(sql, Pinot).sql(Pinot)
493+
494+
# Verify DATE_TRUNC and CAST are preserved
495+
assert "DATE_TRUNC" in result
496+
assert "CAST" in result
497+
498+
# Verify these are NOT converted to MySQL functions
499+
assert "TIMESTAMP(DATETIMECONVERT" not in result
500+
assert result.count("DATE_TRUNC") == 2 # Should appear twice (SELECT and GROUP BY)

0 commit comments

Comments
 (0)