Skip to content

[feature](function) Add Trino-compatible timezone_hour and timezone_m… - #66860

Open
POSSIBLEwbwb wants to merge 5 commits into
apache:masterfrom
POSSIBLEwbwb:timezone-hour-minute
Open

[feature](function) Add Trino-compatible timezone_hour and timezone_m…#66860
POSSIBLEwbwb wants to merge 5 commits into
apache:masterfrom
POSSIBLEwbwb:timezone-hour-minute

Conversation

@POSSIBLEwbwb

Copy link
Copy Markdown

feature Add Trino-compatible timezone_hour and timezone_minute functions

Issue: #48203

Purpose

Add two new scalar functions timezone_hour(timestamp_tz) and timezone_minute(timestamp_tz)
for Trino compatibility. They return the hour / minute component of the timezone offset
of the session timezone at the given instant (DST-aware), consistent with Trino semantics:

  • timezone_hour(timestamp '2024-01-01 00:00:00+08:00') -> 8
  • timezone_hour(timestamp '2024-07-01 00:00:00+00:00') with session tz America/New_York -> -4
  • timezone_minute(timestamp '2024-01-01 00:00:00-04:30') -> -30

Changes

BE

  • be/src/exprs/function/function_timezone_hour_minute.cpp (new)
    • FunctionTimezoneHour / FunctionTimezoneMinute: read context->state()->timezone_obj(),
      compute offset via TimestampTzValue::utc_offset() (DST-aware), return
      offset / 3600 / (offset % 3600) / 60 as Int64.
  • be/src/exprs/function/simple_function_factory.h
    • register both functions.

FE

  • fe/fe-core/.../nereids/trees/expressions/functions/scalar/TimezoneHour.java (new)
  • fe/fe-core/.../nereids/trees/expressions/functions/scalar/TimezoneMinute.java (new)
    • UnaryExpression, ExplicitlyCastableSignature, signature BIGINT <- TIMESTAMP_TZ(WILDCARD),
      PropagateNullable (same pattern as TimeToSec).
  • fe/fe-core/.../nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
    • visit methods.
  • fe/fe-core/.../catalog/BuiltinScalarFunctions.java
    • registration of timezone_hour / timezone_minute.

Regression

  • pytest/qe/palo2/src/test_query_datetime_function.py
    • test_query_timezone_hour_minute: fixed-offset (Asia/Shanghai),
      DST (America/New_York), fractional offsets (-04:30, +05:45), nullable input.

Test plan

  • mvn package -DskipTests -Dskip.doc=true -T 4 -pl fe-core -am — BUILD SUCCESS, checkstyle clean
  • BE unit test function_timezone_hour_minute_test (fixed offset / DST / fractional / nullable)
  • pytest regression test_query_datetime_function.py

License

This contribution is licensed under the Apache License 2.0.

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@linrrzqqq linrrzqqq self-assigned this Aug 18, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes. The review converged after three full rounds with three blocking correctness/test findings, annotated inline.

Checkpoint conclusions:

  • Goal and data correctness: not achieved. The implementation is advertised as Trino-compatible but extracts the session zone after Doris has discarded the input value's zone.
  • Scope, clarity, conditions, and reuse: the FE/BE registration changes are otherwise focused and symmetric, and the const/nullable wrappers are safe; resolving the semantic contract may require a broader representation change or explicit rescoping.
  • Concurrency and lifecycle: the functions add no shared mutable state, locks, ownership cycle, or static-initialization dependency. The unsafe reusable-plan and persisted-MV lifecycles are covered by the second finding.
  • Configuration: no new option is added. Existing time_zone changes reach ordinary distributed and non-folded point-query execution, and SQL result caching keys it correctly; supported BE folding exposes the stale prepared-plan path.
  • Compatibility and parallel paths: FE binding, BIGINT/nullable typing, visitor translation, BE lookup, and ordinary timezone forwarding agree. No storage format or protocol field is added, and no distinct rolling-upgrade defect beyond normal new-builtin availability was found; prepared short-circuit and both MV families remain unsafe.
  • Tests and results: the BE unit test covers fixed, DST, fractional, and nullable session-offset arithmetic, but not the advertised input-zone semantics or the prescribed all-const combinations. The added Python test discards each SET before its SELECT, omits the claimed fractional/nullable end-to-end cases, and there is no standard regression-test result. No matching documentation PR was linked or found. Per the review-task contract, I did not run builds or tests.
  • Observability, transactions, persistence, and writes: no new logging/metrics or base-table transaction/EditLog path is needed; the persisted materialized-view consequence is covered by the second finding.
  • FE/BE variables and performance: no new transmitted variable is introduced, and all ordinary send paths for the existing timezone were checked. Per-row named-zone lookup is needed for DST; no separate CPU or memory issue was substantiated.
  • Other issues and user focus: no additional focus point was provided, and the final missed-issue sweep found no further distinct defect.

auto& result_data = result_column->get_data();
result_data.resize(input_rows_count);

const cctz::time_zone& timezone = context->state()->timezone_obj();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Extract the input value's zone, not the session zone

Trino's timestamp with time zone retains a zone key, and timezone_hour/timezone_minute extract that value's offset. Doris converts an explicit input zone to UTC and discards it, then this line substitutes the session zone. For example, with session +08:00, CAST('2024-01-15 12:00:00-04:30' AS TIMESTAMPTZ) returns 8/0 here instead of Trino's -4/-30. That silently breaks the advertised migration compatibility. Please resolve the contract by retaining/extracting the input zone (including serialization compatibility), or explicitly scope/rename the feature as session-offset extraction, and add an end-to-end case where the input and session zones differ.

/**
* ScalarFunction 'timezone_hour'.
*/
public class TimezoneHour extends ScalarFunction

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Model the session-timezone dependency

Both new functions read session time_zone but inherit isDeterministic() == true. With BE folding enabled, a constant result can be serialized into a reusable prepared point-query plan and remain stale after SET time_zone. The same classification also admits these expressions into synchronous and async materialized views, whose persisted result/rewrite identity does not retain this execution-only variable. Please represent the session dependency so folding, prepared-plan reuse, and MV admission/rewrite all account for it for both classes, and add prepared-query and MV tests across timezone changes.

"""
# UTC+08:00 has no DST, the offset of the session timezone is the same
# for every instant, so timezone_hour always returns 8 here.
runner.init("set time_zone = '+08:00'")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Apply SET time_zone on the SELECT's connection

runner.init() sends the Doris SET through PaloQE.do_sql(), which opens and closes a connection for that call. get_sql_result() then opens a fresh session, so neither SELECT sees the timezone set above it; with the default +08:00, the New York block returns 8/0, not -5/-4. Use the existing do_set_properties_sql(select_sql, ["set time_zone = ..."]) pattern (and avoid modifying the unrelated MySQL session), then add the claimed fractional/nullable and input-zone-vs-session-zone cases in the standard regression suite.

@POSSIBLEwbwb POSSIBLEwbwb Aug 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the thorough review! All three blocking findings are addressed. The branch now contains:

Commit aee93c9b — the three fixes:

[P1] Input-zone semantics — I resolved the contract by making the session-zone semantics explicit (your "explicitly scope" option): a TIMESTAMPTZ value in Doris stores only the UTC instant — the input zone is discarded at parse time (TimestampTzValue is 8 bytes of UTC microseconds, be/src/core/value/timestamptz_value.h), so recovering the input zone requires redesigning the storage format, well beyond this function pair. The scoping is now explicit in:

  • FE javadoc on TimezoneHour / TimezoneMinute (session-zone extraction, divergence from Trino noted).
  • BE comment above the offset extraction in function_timezone_hour_minute.cpp.
  • Documentation (en/zh) with a divergence example: input -04:30, session +08:00 → returns 8/0; Trino would return -4/-30.
  • End-to-end pytest case with differing input/session zones, and BE unit tests const_input + session_zone_wins_over_input_zone.

[P1] Session-timezone dependency (determinism) — modeled on both engines:

  • FE: isDeterministic() overridden to false in TimezoneHour/TimezoneMinute, which sets StatementContext.hasNondeterministic during analysis (same mechanism as now()/current_date()): the statement is excluded from SQL cache, and the expression is not folded into reusable prepared plans or admitted into MV expressions.
  • BE: "timezone_hour"/"timezone_minute" added to NON_DETERMINISTIC_FUNCTIONS in vectorized_fn_call.cpp, so BE-side constant folding skips them too.

[P2] pytest connectiontest_query_timezone_hour_minute rewritten to use do_set_properties_sql, so each SET time_zone and its SELECT run on one connection (this is the established pattern, e.g. test_query_union_join.py). Cases: fixed offset (+08:00), America/New_York winter/summer (DST), fractional (Asia/Kolkata 5/30), input-zone-vs-session-zone divergence (-04:30 input, +08:00 session), and NULL input.

Commit 77bbf781 — robustness: the BE executor now unwraps nullable before const so the column reaches the plain ColumnTimeStampTz data regardless of wrapper order. (Const columns are only legal at the top level of a column tree — Doris enforces this — so the reachable combinations are plain / top-level const / top-level nullable, all covered by the unit tests.)

Verification (local):

  • BE unit tests: 6 FunctionTimezoneHourMinuteTest cases pass against the ASAN UT build (fixed, DST, fractional, const input, session-vs-input zone, nullable).
  • FE compiles cleanly (fe-core, JDK 17 / Maven 3.9).
  • The pytest case was run against a local single-node cluster earlier in this PR's development.

Honest caveats:

  • I did not add prepared-query / MV regression tests: the palo2 pytest suite has no prepared/MV infrastructure, and I have no local regression-framework environment to validate a new .groovy suite (I prefer not to submit unvalidated tests). The determinism fix uses the same standard mechanism as existing non-deterministic builtins; if you'd like, I can follow up with a prepared_stmt_p0 / mv_p0 groovy case once I can run the regression framework locally.
  • The matching documentation update is in [docs](function) add timezone_hour and timezone_minute function docs doris-website#4070 (pushed; not linked in the PR body since I have no token to comment from here).

@POSSIBLEwbwb

Copy link
Copy Markdown
Author

@linrrzqqq Thanks for triggering the first review! I've pushed aee93c9 and
77bbf78 addressing all three blocking findings (session-zone semantics
explicitly scoped, determinism fixed on both FE and BE, pytest SET/connection
fixed).

@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@github-actions

Copy link
Copy Markdown
Contributor

Codex automated review failed and did not complete.

Error: All Codex review accounts are usage-limited; earliest retry is 2026-08-20T03:35:00Z.
Workflow run: https://github.com/apache/doris/actions/runs/32209706914

Please trigger /review again after that time.

@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request changes. Round 1 converged: both normal full-review agents and the separate risk-focused agent returned NO_NEW_VALUABLE_FINDINGS after their missed-issue rechecks. I am adding no new inline comments because the only remaining substantiated correctness defect is an exact duplicate of the existing session-timezone dependency P1.

That P1 is still incompletely resolved. The FE isDeterministic() == false changes correctly prevent FE/BE-fold collection, SQL-cache reuse, default sync/async MV admission, and reuse of the same prepared short-circuit handle. On BE, however, the new names only affect VectorizedFnCall::is_deterministic(). VectorizedFnCall::is_constant() does not consult that method, and both new functions retain the default use_default_implementation_for_constants() == true. Reusable::init() opens the output expressions and populates _constant_col while its new RuntimeState still uses the default +08:00; PointQueryExecutor::init() applies request.time_zone only afterward, and _do_execute() returns the cached value without reevaluation. Thus, under session America/New_York, a full-key short-circuit query projecting timezone_hour(CAST('2024-01-15 12:00:00' AS TIMESTAMPTZ)) can return 8 on its first BE handle instead of -5. The pre-existing nondeterministic functions in this name set also disable default constant handling; these two do not. Please finish the existing P1 by disabling default constant handling for both functions (or making constant caching require determinism / installing the request timezone before expression open), and add the already-requested literal point-query coverage.

Checkpoint conclusions:

  • Goal and data correctness: ordinary session-zone offset extraction, DST, fractional offsets, null propagation, and FE/BE BIGINT typing are correct. The goal is not complete because the short-circuit first-open path can return an offset for the default zone rather than the request zone.
  • Scope, clarity, reuse, and special conditions: registration and visitor changes are focused and symmetric. The explicit session-zone contract and Trino divergence are now documented in FE/BE code and tested. The wrapper-order comment is broader than the legal shapes, but framework normalization makes the implementation safe.
  • Concurrency: the functions add no shared mutable state or locks, and no deadlock or lock-order issue was found. The blocking defect is expression-cache lifecycle ordering, not a newly introduced data race.
  • Lifecycle and static initialization: no ownership cycle, cleanup leak, or cross-TU static-initialization dependency was found. The reusable point-query expression lifetime is the unresolved issue described above.
  • Configuration: no new configuration is added. Ordinary execution observes the existing session time_zone; the point-query cache is populated before that existing value is installed.
  • Compatibility and parallel paths: the change is additive, FE/BE signatures agree, and it introduces no storage-format or protocol field. Regular distributed execution is correct; the short-circuit point-query path is not. The function is explicitly session-offset extraction rather than preservation of Trino's input-zone semantics.
  • Tests and results: the BE unit test covers fixed, DST, fractional, const, nullable, and session-vs-input-zone cases, and the pytest now keeps each SET and SELECT on one connection. Prepared/MV and standard regression-test coverage requested by the existing thread is still absent. Per the review task, I did not run builds or tests; live checks show the build-all job was skipped, so current CI does not independently prove compilation or execution.
  • Observability: no new log, metric, or tracing requirement was identified for these local scalar functions.
  • Transactions, persistence, and writes: no base-table write, transaction, visible-version, EditLog, or delete-bitmap path is modified. Default MV admission now recognizes nondeterminism; its missing regression coverage is already tracked in the existing thread.
  • FE/BE variables: no new transmitted variable is introduced. The existing timezone request is forwarded, but the BE reusable lifecycle consumes it too late for cached literal expressions.
  • Performance: the per-row named-zone lookup is required for DST-aware offsets; no separate material CPU, allocation, or memory-accounting problem was substantiated.
  • Other issues and user focus: no additional user focus was provided. The final changed-file and unresolved-candidate sweep found no further distinct issue, and the prior input-zone scoping and pytest connection concerns are addressed without duplicating their threads.

@linrrzqqq

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review status: complete after two convergence rounds. Requesting changes for one correctness issue and one performance issue.

Summary

The FE/BE registration, TIMESTAMPTZ(*) -> BIGINT signatures, nullable propagation, signed fractional offsets, and ordinary DST lookup are consistent. The input-zone versus session-zone behavior is now explicitly documented as a Doris divergence, and the pytest connection handling is fixed. Point-query initialization still permits a constant descendant to be cached before the session timezone is applied (P1), and constant inputs do redundant O(N) lookup and materialization work (P2).

Critical checkpoints

  • Goal and proof: direct BE and pytest coverage exercises fixed, DST, fractional, nullable, constant, and input-zone-divergence cases, but the point-query result is not correct for the nested constant path in MAIN-001. A nested DST point-query regression is required. The prepared/MV test gap is already tracked in an existing thread and is not duplicated here.
  • Scope and clarity: the nine-file change is focused and the shared helper and registration are clear; the remaining constant-cache policy is incomplete, and the const branch is unnecessarily row-wise.
  • Concurrency: no new shared mutable state or lock ordering was introduced. RuntimeState is query-local; the point-query reusable lifecycle and initialization ordering is the correctness issue.
  • Lifecycle and ownership: the function objects are stateless and introduce no ownership cycle. Opening the expression tree before installing the request timezone is MAIN-001.
  • Configuration: no new setting is introduced; the feature uses the existing session time_zone propagation.
  • Compatibility: FE/BE names and types align, with no storage or wire-format change. Doris's loss of the input zone is now documented rather than presented as Trino-equivalent behavior.
  • Parallel paths: regular execution, FE/BE folding, SQL cache, prepared reuse, synchronous and asynchronous MV handling, and point-query execution were traced. The outer nondeterminism gates work, but a constant descendant escapes them.
  • Conditions and error handling: type and nullability invariants use the existing function framework, null maps are preserved, and no Status is ignored.
  • Tests: the added tests cover the direct cases above. Per the review automation instructions, I did not run builds or tests. Add the nested point-query regression from P1 and assert the block-local physical const shape for P2.
  • Observability: these pure scalar functions do not require new logs or metrics.
  • Persistence, transactions, writes, and FE/BE variables: no new persistence, transactional, or write path exists; the only cross-layer variable is the existing timezone request.
  • Performance: MAIN-002 identifies avoidable O(N) cctz lookups and result materialization for a block-constant input.
  • User focus: no additional focus area was specified.

// inputs both reach the plain ColumnTimeStampTz data below.
col = remove_nullable(col);
if (is_column_const(*col)) {
col = assert_cast<const ColumnConst&>(*col).convert_to_full_column();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Preserve block-local constness without enabling cross-request caching

Because the framework constant path is disabled, this expands a one-value ColumnConst to input_rows_count, allocates a full result column, and performs the identical cctz lookup for every scanned row. A projection such as timezone_hour(CAST('2024-01-15 12:00:00' AS TIMESTAMPTZ)) over a large table therefore does O(N) timezone work for one per-execution value. Keep use_default_implementation_for_constants() false so VectorizedFnCall::is_constant() cannot cache across requests, but detect the const argument here, evaluate its nested value once, and return a block-local ColumnConst; the const-input test can assert that physical shape.

// columns without re-evaluating them (VectorizedFnCall::is_constant
// consults this flag). Disable it like other nondeterministic functions
// (e.g. random, uuid).
bool use_default_implementation_for_constants() const override { return false; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Apply the request timezone before constant children open

This override only makes the outer call nonconstant. VectorizedFnCall::open() still opens its children first, and a constant VCastExpr caches itself through VExpr::get_const_col(). In the point-query path, Reusable::init() opens the expression tree while its RuntimeState still has Doris's default +08:00; PointQueryExecutor::init() applies request->time_zone only afterward. For example, with session America/New_York, timezone_hour(CAST(least('2024-03-10 03:30:00','2024-03-11 03:30:00') AS TIMESTAMPTZ)) survives FE folding because least has no FE evaluator. The child cast is cached as 2024-03-09 19:30 UTC under +08:00, so the outer call returns -5; parsing the selected value in New York gives 2024-03-10 07:30 UTC and should return -4. This is distinct from the existing outer-result cache thread: the stale value is a constant descendant and is wrong on the first point-query request. Please install the request timezone before opening or caching the point-query expression tree and add this nested-constant DST regression.

… and const fast path for timezone_hour/minute

The point-query short-circuit executor opened the output expressions with
the default +08:00 timezone and applied the request timezone only
afterwards, so constant descendants (e.g. a const VCastExpr above a
session-timezone-dependent function) cached a value evaluated with the
wrong zone. Install the request timezone in Reusable::init() before the
expressions are prepared/opened.

With the framework constant path disabled for timezone_hour/
timezone_minute, a block-local const argument was expanded to
input_rows_count rows and re-evaluated per row; compute the single value
once and keep the const shape of the result.
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.

3 participants