Skip to content

DATE_TRUNC: auto-cast unknown-typed time-dimension expressions#140

Merged
AivanF merged 1 commit into
mainfrom
aivan/2026-05-22-DATE_TRUNC-typing
May 25, 2026
Merged

DATE_TRUNC: auto-cast unknown-typed time-dimension expressions#140
AivanF merged 1 commit into
mainfrom
aivan/2026-05-22-DATE_TRUNC-typing

Conversation

@AivanF
Copy link
Copy Markdown
Collaborator

@AivanF AivanF commented May 22, 2026

A SlayerModel time dimension whose sql is a bare string literal (Column(sql="'2025-12-01'", type=TIMESTAMP)) — a real pattern for synthetic/reference-data time spines — generated SQL like DATE_TRUNC('month', '2025-12-01'), which Postgres rejects at execution time with function date_trunc(unknown, unknown) is not unique because the second argument carries no resolvable type and there's no single overload to pick.

This PR fixes the SQL generator's single _build_date_trunc helper to wrap non-column, non-already-cast operands in CAST(... AS TIMESTAMP) before passing them to DATE_TRUNC — bare column references stay untouched (their live DB type is already known, and forcing a cast there could silently strip TIMESTAMPTZ to TIMESTAMP).

Because all DATE_TRUNC emission sites already funnel through that one helper, the fix simultaneously covers regular SELECTs, shifted CTEs, the compare-period builder, and time-bucket window functions. Added a parametrized regression test across postgres / mysql / bigquery / duckdb / snowflake asserting both directions: bare-literal time dims get wrapped, bare columns don't.

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced SQL query reliability for date truncation operations across supported database platforms (PostgreSQL, MySQL, BigQuery, DuckDB, Snowflake).
  • Tests

    • Added test coverage for date truncation functionality across multiple database systems.

Review Change Stack

A `SlayerModel` time dimension whose `sql` is a bare string literal (`Column(sql="'2025-12-01'", type=TIMESTAMP)`) — a real pattern for synthetic/reference-data time spines — generated SQL like `DATE_TRUNC('month', '2025-12-01')`, which Postgres rejects at execution time with `function date_trunc(unknown, unknown) is not unique` because the second argument carries no resolvable type and there's no single overload to pick. This PR fixes the SQL generator's single `_build_date_trunc` helper to wrap non-column, non-already-cast operands in `CAST(... AS TIMESTAMP)` before passing them to `DATE_TRUNC` — bare column references stay untouched (their live DB type is already known, and forcing a cast there could silently strip `TIMESTAMPTZ` to `TIMESTAMP`). Because all DATE_TRUNC emission sites already funnel through that one helper, the fix simultaneously covers regular SELECTs, shifted CTEs, the compare-period builder, and time-bucket window functions. Added a parametrized regression test across `postgres / mysql / bigquery / duckdb / snowflake` asserting both directions: bare-literal time dims get wrapped, bare columns don't.
@AivanF AivanF requested a review from ZmeiGorynych May 22, 2026 11:11
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

SQLGenerator._build_date_trunc now enforces stricter typing by wrapping non-Column, non-Cast expressions in CAST(... AS TIMESTAMP) before generating DATE_TRUNC AST to resolve Postgres planner ambiguity. Docstring expanded and multi-dialect test added.

Changes

DATE_TRUNC Type Casting

Layer / File(s) Summary
DATE_TRUNC type casting logic
slayer/sql/generator.py
Docstring clarifies the cast behavior: non-Column/non-Cast inputs are cast to TIMESTAMP before DATE_TRUNC to disambiguate "unknown" typed operands; bare columns and existing casts are left unchanged. A new guard in _build_date_trunc wraps col_expr with CAST(... AS TIMESTAMP) when neither condition is met.
Multi-dialect test validation
tests/test_sql_generator.py
Parameterized regression test across five dialects (postgres, mysql, bigquery, duckdb, snowflake) verifies that bare literal date strings are cast to TIMESTAMP while bare timestamp column references are not cast, using substring assertions on generated SQL.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A rabbit hops through types with care,
Casting dates to timestamp's pair,
Unknown strings now safely caught,
In TIMESTAMP's protective thought,
Five dialects tested, none left bare!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: auto-casting of unknown-typed time-dimension expressions in DATE_TRUNC. It directly matches the primary objective and the core code modification.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aivan/2026-05-22-DATE_TRUNC-typing

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_sql_generator.py (1)

1926-1926: ⚡ Quick win

Narrow the no-CAST assertion to the DATE_TRUNC operand

assert "CAST(" not in sql.upper() is too broad and can fail on unrelated CASTs added elsewhere in query generation. Assert specifically that the bare-column operand path is uncast (e.g., no CAST(created_at AS ...) near the truncation expression).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sql_generator.py` at line 1926, The test currently asserts
globally that no "CAST(" appears in sql (assert "CAST(" not in sql.upper()),
which is too broad; instead locate the DATE_TRUNC/DATETRUNC expression in the
generated sql and assert that its operand is the bare column (e.g., not wrapped
in a CAST). Update the assertion to parse the sql string (using a regex) to find
the DATE_TRUNC/DATETRUNC call and then assert that there is no pattern like
CAST\s*\(\s*created_at\s+AS\b (case-insensitive) inside that truncation
expression; operate on the variable sql and keep the check case-insensitive so
unrelated CASTs elsewhere are allowed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_sql_generator.py`:
- Line 1926: The test currently asserts globally that no "CAST(" appears in sql
(assert "CAST(" not in sql.upper()), which is too broad; instead locate the
DATE_TRUNC/DATETRUNC expression in the generated sql and assert that its operand
is the bare column (e.g., not wrapped in a CAST). Update the assertion to parse
the sql string (using a regex) to find the DATE_TRUNC/DATETRUNC call and then
assert that there is no pattern like CAST\s*\(\s*created_at\s+AS\b
(case-insensitive) inside that truncation expression; operate on the variable
sql and keep the check case-insensitive so unrelated CASTs elsewhere are
allowed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 247ee27c-e68a-4dae-9aff-57c8c083d2ad

📥 Commits

Reviewing files that changed from the base of the PR and between a8b8f2c and aefc25f.

📒 Files selected for processing (2)
  • slayer/sql/generator.py
  • tests/test_sql_generator.py

@AivanF AivanF merged commit ebd913f into main May 25, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants