diff --git a/docs/en/sql-reference/data-types/time.md b/docs/en/sql-reference/data-types/time.md index e4434e63ca8c..9790fbfefa98 100644 --- a/docs/en/sql-reference/data-types/time.md +++ b/docs/en/sql-reference/data-types/time.md @@ -80,6 +80,7 @@ SELECT * FROM tab ORDER BY event_id; **2.** Filtering on `Time` values ``` sql +SET use_legacy_to_time = 0; SELECT * FROM tab WHERE time = toTime('14:30:25') ``` @@ -115,6 +116,23 @@ SELECT CAST('14:30:25' AS Time) AS column, toTypeName(column) AS type └───────────┴──────┘ ``` +## Addition with Date {#addition-with-date} + +A [Time](time.md) value can be added to a [Date](date.md) or [Date32](date32.md) value to produce a [DateTime](datetime.md) or [DateTime64](datetime64.md): + +```sql +SET use_legacy_to_time = 0; +SELECT toDate('2024-07-15') + toTime('14:30:25') as datetime; +``` + +```text + ┌────────────datetime─┐ +1. │ 2024-07-15 14:30:25 │ + └─────────────────────┘ +``` + +See [Date and Time Addition](../operators/index.md#date-time-addition) for details on all supported combinations and result types. + ## See Also {#see-also} - [Type conversion functions](../functions/type-conversion-functions.md) diff --git a/docs/en/sql-reference/data-types/time64.md b/docs/en/sql-reference/data-types/time64.md index a4c07fe2b2a2..c84bb40d90fb 100644 --- a/docs/en/sql-reference/data-types/time64.md +++ b/docs/en/sql-reference/data-types/time64.md @@ -121,6 +121,23 @@ SELECT CAST('14:30:25.250' AS Time64(3)) AS column, toTypeName(column) AS type; └───────────────┴───────────┘ ``` +## Addition with Date {#addition-with-date} + +A [Time64](time64.md) value can be added to a [Date](date.md) or [Date32](date32.md) value to produce a [DateTime64](datetime64.md) with the same scale as the `Time64`: + +```sql +SET use_legacy_to_time = 0; +SELECT toDate('2024-07-15') + toTime64('14:30:25.123456', 6) AS dt, toTypeName(dt); +``` + +```text + ┌─────────────────────────dt─┬─toTypeName(dt)─┐ +1. │ 2024-07-15 14:30:25.123456 │ DateTime64(6) │ + └────────────────────────────┴────────────────┘ +``` + +See [Date and Time Addition](../operators/index.md#date-time-addition) for details on all supported combinations and result types. + **See Also** - [Type conversion functions](../../sql-reference/functions/type-conversion-functions.md) diff --git a/docs/en/sql-reference/operators/index.md b/docs/en/sql-reference/operators/index.md index c358300f1e61..c40045874e5e 100644 --- a/docs/en/sql-reference/operators/index.md +++ b/docs/en/sql-reference/operators/index.md @@ -102,7 +102,7 @@ SELECT * FROM a AS a1 JOIN a AS a2 ON a1.x <=> a2.x; ::: The `<=>` operator is the `NULL`-safe equality operator, equivalent to `IS NOT DISTINCT FROM`. -It works like the regular equality operator (`=`), but it treats `NULL` values as comparable. +It works like the regular equality operator (`=`), but it treats `NULL` values as comparable. Two `NULL` values are considered equal, and a `NULL` compared to any non-`NULL` value returns 0 (false) rather than `NULL`. ```sql @@ -134,7 +134,7 @@ See [IN operators](../../sql-reference/operators/in.md) and [EXISTS](../../sql-r `a GLOBAL NOT IN ...` – The `globalNotIn(a, b)` function. ### in subquery function {#in-subquery-function} -`a = ANY (subquery)` – The `in(a, subquery)` function. +`a = ANY (subquery)` – The `in(a, subquery)` function. ### notIn subquery function {#notin-subquery-function} `a != ANY (subquery)` – The same as `a NOT IN (SELECT singleValueOrNull(*) FROM subquery)`. @@ -143,7 +143,7 @@ See [IN operators](../../sql-reference/operators/in.md) and [EXISTS](../../sql-r `a = ALL (subquery)` – The same as `a IN (SELECT singleValueOrNull(*) FROM subquery)`. ### notIn subquery function {#notin-subquery-function-1} -`a != ALL (subquery)` – The `notIn(a, subquery)` function. +`a != ALL (subquery)` – The `notIn(a, subquery)` function. **Examples** @@ -278,7 +278,7 @@ Types of intervals: You can also use a string literal when setting the `INTERVAL` value. For example, `INTERVAL 1 HOUR` is identical to the `INTERVAL '1 hour'` or `INTERVAL '1' hour`. -:::tip +:::tip Intervals with different types can't be combined. You can't use expressions like `INTERVAL 4 DAY 1 HOUR`. Specify intervals in units that are smaller or equal to the smallest unit of the interval, for example, `INTERVAL 25 HOUR`. You can use consecutive operations, like in the example below. ::: @@ -314,7 +314,7 @@ SELECT now() AS current_date_time, current_date_time + INTERVAL '4' day + INTERV └─────────────────────┴────────────────────────────────────────────────────────────┘ ``` -:::note +:::note The `INTERVAL` syntax or `addDays` function are always preferred. Simple addition or subtraction (syntax like `now() + ...`) doesn't consider time settings. For example, daylight saving time. ::: @@ -335,6 +335,56 @@ SELECT toDateTime('2014-10-26 00:00:00', 'Asia/Istanbul') AS time, time + 60 * 6 - [Interval](../../sql-reference/data-types/special-data-types/interval.md) data type - [toInterval](/sql-reference/functions/type-conversion-functions#toIntervalYear) type conversion functions +### Date and Time Addition {#date-time-addition} + +A [Date](../../sql-reference/data-types/date.md) or [Date32](../../sql-reference/data-types/date32.md) value can be added to a [Time](../../sql-reference/data-types/time.md) or [Time64](../../sql-reference/data-types/time64.md) value using the `+` operator. The result is a [DateTime](../../sql-reference/data-types/datetime.md) or [DateTime64](../../sql-reference/data-types/datetime64.md) representing the date at the given time of day. The operation is commutative. + +The result type depends on the operand types: + +| Left operand | Right operand | Result type | +|---|---|---| +| `Date` | `Time` | `DateTime` | +| `Date` | `Time64(s)` | `DateTime64(s)` | +| `Date32` | `Time` | `DateTime64(0)` | +| `Date32` | `Time64(s)` | `DateTime64(s)` | + +:::note +The result uses the [session timezone](../../operations/settings/settings.md#session_timezone) (or server default timezone if no session timezone is set). The [`date_time_overflow_behavior`](../../operations/settings/settings-formats.md#date_time_overflow_behavior) setting controls what happens when the result is outside the representable range. +::: + +Examples: + +```sql +SET use_legacy_to_time = 0; +SELECT toDate('2024-07-15') + toTime('14:30:25') AS dt, toTypeName(dt); +``` + +```text +┌──────────────────dt─┬─toTypeName(dt)─┐ +│ 2024-07-15 14:30:25 │ DateTime │ +└─────────────────────┴────────────────┘ +``` + +```sql +SELECT toDate('2024-07-15') + toTime64('14:30:25.123456', 6) AS dt, toTypeName(dt); +``` + +```text +┌─────────────────────────dt─┬─toTypeName(dt)─┐ +│ 2024-07-15 14:30:25.123456 │ DateTime64(6) │ +└────────────────────────────┴────────────────┘ +``` + +```sql +SELECT toTime64('23:59:59.999', 3) + toDate32('2024-07-15') AS dt, toTypeName(dt); +``` + +```text +┌──────────────────────dt─┬─toTypeName(dt)─┐ +│ 2024-07-15 23:59:59.999 │ DateTime64(3) │ +└─────────────────────────┴────────────────┘ +``` + ## Logical AND Operator {#logical-and-operator} Syntax `SELECT a AND b` — calculates logical conjunction of `a` and `b` with the function [and](/sql-reference/functions/logical-functions#and). diff --git a/src/Functions/FunctionBinaryArithmetic.h b/src/Functions/FunctionBinaryArithmetic.h index 517acc0d0904..1aac4768828c 100644 --- a/src/Functions/FunctionBinaryArithmetic.h +++ b/src/Functions/FunctionBinaryArithmetic.h @@ -40,6 +40,8 @@ #include #include #include +#include +#include #include #include #include @@ -80,8 +82,11 @@ namespace ErrorCodes extern const int CANNOT_ADD_DIFFERENT_AGGREGATE_STATES; extern const int NUMBER_OF_ARGUMENTS_DOESNT_MATCH; extern const int SIZES_OF_ARRAYS_DONT_MATCH; + extern const int VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE; } +FormatSettings::DateTimeOverflowBehavior getDateTimeOverflowBehavior(ContextPtr context); + namespace traits_ { struct InvalidType; /// Used to indicate undefined operation @@ -1143,6 +1148,18 @@ class FunctionBinaryArithmetic : public IFunction return which1.isTime64() && which0.isTimeOrTime64(); } + static bool isDateAndTimeAddition(const DataTypePtr & type0, const DataTypePtr & type1) + { + if constexpr (!is_plus) + return false; + + WhichDataType which0(type0); + WhichDataType which1(type1); + + return (which0.isDateOrDate32() && which1.isTimeOrTime64()) + || (which0.isTimeOrTime64() && which1.isDateOrDate32()); + } + /// Multiply aggregation state by integer constant: by merging it with itself specified number of times. ColumnPtr executeAggregateMultiply(const ColumnsWithTypeAndName & arguments, const DataTypePtr &, size_t input_rows_count) const { @@ -1439,6 +1456,202 @@ class FunctionBinaryArithmetic : public IFunction return col_res; } + /// Executes Date/Date32 + Time/Time64 -> DateTime/DateTime64. + /// + /// Converts the date to a midnight unix timestamp, then adds the time-of-day offset. + /// For Time64, the midnight timestamp is scaled to match Time64's precision (e.g. milliseconds) + /// before adding. The arithmetic uses Int128 so that the scaling cannot overflow. + /// + /// Result type: Date + Time -> DateTime, all other combinations -> DateTime64(scale). + /// Overflow: respects date_time_overflow_behavior (throw / saturate / ignore). + ColumnPtr executeDateAndTimeAddition(const ColumnsWithTypeAndName & arguments, const DataTypePtr & result_type, size_t input_rows_count) const + { + /// The operation is commutative — figure out which argument is the date and which is the time. + WhichDataType which0(arguments[0].type); + size_t date_idx = which0.isDateOrDate32() ? 0 : 1; + const auto & date_arg = arguments[date_idx]; + const auto & time_arg = arguments[1 - date_idx]; + + bool is_date32 = WhichDataType(date_arg.type).isDate32(); + bool is_time64 = WhichDataType(time_arg.type).isTime64(); + + /// Date + Time -> DateTime (UInt32 seconds). All other combinations -> DateTime64 + /// because either Date32's range exceeds UInt32, or Time64 requires sub-second precision. + bool result_is_datetime64 = is_date32 || is_time64; + + /// Session timezone if set, otherwise server default. Used by fromDayNum to convert + /// a day number to a midnight unix timestamp — different timezones give different timestamps. + const auto & time_zone = DateLUT::instance(); + + /// Time64 values are stored in scaled units (e.g. milliseconds at scale 3, nanoseconds at scale 9). + /// The midnight timestamp is in seconds, so we multiply it by this factor to match Time64's units. + /// For plain Time (seconds), scale_multiplier stays 1. + Int64 scale_multiplier = 1; + if (is_time64) + { + const auto * time64_type = checkAndGetDataType(time_arg.type.get()); + scale_multiplier = DecimalUtils::scaleMultiplier(time64_type->getScale()); + } + + auto overflow_behavior = getDateTimeOverflowBehavior(context); + + /// The valid range for the result, expressed in the result's own units: + /// DateTime: seconds in [0, 2^32-1], covering ~1970 to ~2106. + /// DateTime64: scaled values in [MIN_DATETIME64_TIMESTAMP * 10^scale, MAX_DATETIME64_TIMESTAMP * 10^scale], + /// covering ~1900 to ~2299 (but narrower at scale 9 due to Int64 capacity). + Int64 result_min = 0; + Int64 result_max = static_cast(MAX_DATETIME_TIMESTAMP); + UInt32 result_scale = 0; + if (result_is_datetime64) + { + const auto & res_type = assert_cast(*result_type); + result_scale = res_type.getScale(); + Int64 result_scale_mul = DecimalUtils::scaleMultiplier(result_scale); + /// The min side (1900-01-01) fits in Int64 for all supported scales (0-9). + /// The max side (2299-12-31) overflows Int64 at scale 9; clamp to Int64 max in that case. + result_min = MIN_DATETIME64_TIMESTAMP * result_scale_mul; + result_max = (MAX_DATETIME64_TIMESTAMP <= std::numeric_limits::max() / result_scale_mul) + ? MAX_DATETIME64_TIMESTAMP * result_scale_mul + result_scale_mul - 1 + : std::numeric_limits::max(); + } + + /// Convert a day number to the unix timestamp (seconds) at midnight of that day. + auto to_midnight = [&](Int64 day_num) -> Int64 + { + return is_date32 ? static_cast(time_zone.fromDayNum(ExtendedDayNum(static_cast(day_num)))) + : static_cast(time_zone.fromDayNum(DayNum(static_cast(day_num)))); + }; + + /// Check whether the computed result fits in the valid range. + /// The result comes in as Int128 (which cannot overflow), and is narrowed to Int64 here. + auto check_and_clamp = [&](Int128 wide_result) -> Int64 + { + if (wide_result >= result_min && wide_result <= result_max) [[likely]] + return static_cast(wide_result); + + if (overflow_behavior == FormatSettings::DateTimeOverflowBehavior::Throw) + { + if (result_is_datetime64) + throw Exception( + ErrorCodes::VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE, + "The result of Date plus Time is out of bounds of type DateTime64({})", + result_scale); + else + throw Exception( + ErrorCodes::VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE, + "Value {} is out of bounds of type DateTime", + static_cast(wide_result)); + } + else if (overflow_behavior == FormatSettings::DateTimeOverflowBehavior::Saturate) + { + return static_cast(std::clamp(wide_result, result_min, result_max)); + } + /// Ignore: truncate to Int64. The result is undefined per ClickHouse docs. + return static_cast(wide_result); + }; + + /// Full computation: look up midnight from the day number, scale it, add time, check bounds. + auto compute_from_day = [&](Int64 day_num, Int64 time_val) -> Int64 + { + Int64 midnight_ts = to_midnight(day_num); + + if (is_time64) + return check_and_clamp(Int128(midnight_ts) * scale_multiplier + time_val); + + return check_and_clamp(Int128(midnight_ts) + time_val); + }; + + /// When the date is a constant, midnight (already scaled) is precomputed before the loop. + /// This skips the day-number lookup and the multiply on every row. + auto compute_from_precomputed_midnight + = [&](Int128 midnight_wide, Int64 time_val) -> Int64 { return check_and_clamp(midnight_wide + time_val); }; + + /// Resolve column data to typed raw pointers once, so the per-row loop + /// only does pointer indexing — no dynamic_cast or type checks per row. + const auto * date_col = date_arg.column.get(); + const auto * time_col = time_arg.column.get(); + bool date_is_const = isColumnConst(*date_col); + bool time_is_const = isColumnConst(*time_col); + + Int64 date_const_value = 0; + const Int32 * date32_ptr = nullptr; + const UInt16 * date16_ptr = nullptr; + if (date_is_const) + date_const_value = is_date32 ? checkAndGetColumnConst>(date_col)->template getValue() + : checkAndGetColumnConst>(date_col)->template getValue(); + else if (is_date32) + date32_ptr = checkAndGetColumn>(date_col)->getData().data(); + else + date16_ptr = checkAndGetColumn>(date_col)->getData().data(); + + Int64 time_const_value = 0; + const Time64 * time64_ptr = nullptr; + const Int32 * time32_ptr = nullptr; + if (time_is_const) + time_const_value = is_time64 ? checkAndGetColumnConst>(time_col)->template getValue().value + : checkAndGetColumnConst>(time_col)->template getValue(); + else if (is_time64) + time64_ptr = checkAndGetColumn>(time_col)->getData().data(); + else + time32_ptr = checkAndGetColumn>(time_col)->getData().data(); + + auto get_date = [&](size_t i) -> Int64 + { + if (date_is_const) + return date_const_value; + return is_date32 ? static_cast(date32_ptr[i]) : static_cast(date16_ptr[i]); + }; + auto get_time = [&](size_t i) -> Int64 + { + if (time_is_const) + return time_const_value; + return is_time64 ? time64_ptr[i].value : static_cast(time32_ptr[i]); + }; + + /// When the date column is constant, compute midnight once (as Int128, already scaled + /// for Time64) so the loop only needs to add each row's time value. + Int128 midnight_precomputed = 0; + if (date_is_const) + { + Int64 midnight_ts = to_midnight(date_const_value); + midnight_precomputed = is_time64 ? Int128(midnight_ts) * scale_multiplier : Int128(midnight_ts); + } + + /// Both columns constant — compute once, return a single-value column. + if (date_is_const && time_is_const) + { + Int64 const_result = compute_from_precomputed_midnight(midnight_precomputed, time_const_value); + if (result_is_datetime64) + return result_type->createColumnConst(input_rows_count, DecimalField(DateTime64(const_result), result_scale)); + return result_type->createColumnConst(input_rows_count, static_cast(static_cast(const_result))); + } + + if (result_is_datetime64) + { + auto col_res = ColumnDecimal::create(input_rows_count, result_scale); + auto & result_data = col_res->getData(); + if (date_is_const) + for (size_t i = 0; i < input_rows_count; ++i) + result_data[i] = DateTime64(compute_from_precomputed_midnight(midnight_precomputed, get_time(i))); + else + for (size_t i = 0; i < input_rows_count; ++i) + result_data[i] = DateTime64(compute_from_day(get_date(i), get_time(i))); + return col_res; + } + else + { + auto col_res = ColumnVector::create(input_rows_count); + auto & result_data = col_res->getData(); + if (date_is_const) + for (size_t i = 0; i < input_rows_count; ++i) + result_data[i] = static_cast(compute_from_precomputed_midnight(midnight_precomputed, get_time(i))); + else + for (size_t i = 0; i < input_rows_count; ++i) + result_data[i] = static_cast(compute_from_day(get_date(i), get_time(i))); + return col_res; + } + } + ColumnPtr executeDateTimeIntervalPlusMinus(const ColumnsWithTypeAndName & arguments, const DataTypePtr & result_type, size_t input_rows_count, const FunctionOverloadResolverPtr & function_builder) const { @@ -1873,6 +2086,42 @@ class FunctionBinaryArithmetic : public IFunction } return std::make_shared(DecimalUtils::max_precision, std::max(scale_lhs, scale_rhs)); } + else if (isDateAndTimeAddition(arguments[0], arguments[1])) /// Special case when the function is plus, one argument is Date/Date32 and the other is Time/Time64. + { + WhichDataType which0(arguments[0]); + WhichDataType which1(arguments[1]); + + const WhichDataType & date_which = which0.isDateOrDate32() ? which0 : which1; + const WhichDataType & time_which = which0.isTimeOrTime64() ? which0 : which1; + const DataTypePtr & time_type = which0.isTimeOrTime64() ? arguments[0] : arguments[1]; + + if (date_which.isDate() && time_which.isTime()) + { + return std::make_shared(); + } + else if (date_which.isDate() && time_which.isTime64()) + { + const auto * t64 = checkAndGetDataType(time_type.get()); + return std::make_shared(t64->getScale()); + } + else if (date_which.isDate32() && time_which.isTime()) + { + return std::make_shared(0); + } + else if (date_which.isDate32() && time_which.isTime64()) + { + const auto * t64 = checkAndGetDataType(time_type.get()); + return std::make_shared(t64->getScale()); + } + else + { + throw Exception( + ErrorCodes::LOGICAL_ERROR, + "Unexpected combination of date and time types for plus: {} and {}", + arguments[0]->getName(), + arguments[1]->getName()); + } + } if constexpr (is_multiply || is_division) { @@ -2567,6 +2816,12 @@ ColumnPtr executeStringInteger(const ColumnsWithTypeAndName & arguments, const A return executeTime64Subtraction(arguments, result_type, input_rows_count); } + /// Special case when the function is plus, one argument is Date/Date32 and the other is Time/Time64. + if (isDateAndTimeAddition(arguments[0].type, arguments[1].type)) + { + return executeDateAndTimeAddition(arguments, result_type, input_rows_count); + } + /// Special case when the function is plus or minus, one of arguments is Date/DateTime/String and another is Interval. if (auto function_builder = getFunctionForIntervalArithmetic(arguments[0].type, arguments[1].type, context)) { diff --git a/src/Functions/plus.cpp b/src/Functions/plus.cpp index f54fd5019bc3..c7f79615b61c 100644 --- a/src/Functions/plus.cpp +++ b/src/Functions/plus.cpp @@ -1,10 +1,24 @@ #include #include +#include +#include #include namespace DB { +namespace Setting +{ +extern const SettingsDateTimeOverflowBehavior date_time_overflow_behavior; +} + +FormatSettings::DateTimeOverflowBehavior getDateTimeOverflowBehavior(ContextPtr context) +{ + if (context) + return context->getSettingsRef()[Setting::date_time_overflow_behavior].value; + return default_date_time_overflow_behavior; +} + template struct PlusImpl { @@ -55,6 +69,9 @@ Calculates the sum of two values `x` and `y`. Alias: `x + y` (operator). It is possible to add an integer and a date or date with time. The former operation increments the number of days in the date, the latter operation increments the number of seconds in the date with time. +It is also possible to add a date and a time. Adding a `Date` and a `Time` +produces a `DateTime`. Adding a `Date` and a `Time64`, or a `Date32` and +a `Time` or `Time64`, produces a `DateTime64`. )"; FunctionDocumentation::Syntax syntax = "plus(x, y)"; FunctionDocumentation::Argument argument1 = {"x", "Left hand operand."}; @@ -63,7 +80,8 @@ increments the number of seconds in the date with time. FunctionDocumentation::ReturnedValue returned_value = {"Returns the sum of x and y"}; FunctionDocumentation::Example example1 = {"Adding two numbers", "SELECT plus(5,5)", "10"}; FunctionDocumentation::Example example2 = {"Adding an integer and a date", "SELECT plus(toDate('2025-01-01'),5)", "2025-01-06"}; - FunctionDocumentation::Examples examples = {example1, example2}; + FunctionDocumentation::Example example3 = {"Adding a date and time", "SELECT toDate('2025-01-01') + CAST('14:30:25', 'Time')", "2025-01-01 14:30:25"}; + FunctionDocumentation::Examples examples = {example1, example2, example3}; FunctionDocumentation::Category category = FunctionDocumentation::Category::Arithmetic; FunctionDocumentation::IntroducedIn introduced_in = {1, 1}; FunctionDocumentation documentation = {description, syntax, arguments, {}, returned_value, examples, introduced_in, category}; diff --git a/tests/queries/0_stateless/04092_date_plus_time.reference b/tests/queries/0_stateless/04092_date_plus_time.reference new file mode 100644 index 000000000000..c0776c7e57d1 --- /dev/null +++ b/tests/queries/0_stateless/04092_date_plus_time.reference @@ -0,0 +1,502 @@ +-- { echo } + +SET enable_time_time64_type = 1; +SET use_legacy_to_time = 0; +SET session_timezone = 'UTC'; +SET date_time_overflow_behavior = 'throw'; +-- Date + Time -> DateTime +SELECT toDate('2024-01-15') + toTime('01:02:03') AS dt, toTypeName(dt); +2024-01-15 01:02:03 DateTime +-- Date + Time64 -> DateTime64 +SELECT toDate('2024-01-15') + toTime64('01:02:03.456', 3) AS dt, toTypeName(dt); +2024-01-15 01:02:03.456 DateTime64(3) +-- Date32 + Time -> DateTime64(0) +SELECT toDate32('2024-01-15') + toTime('01:02:03') AS dt, toTypeName(dt); +2024-01-15 01:02:03 DateTime64(0) +-- Date32 + Time64 -> DateTime64(6) +SELECT toDate32('2024-01-15') + toTime64('01:02:03.456789', 6) AS dt, toTypeName(dt); +2024-01-15 01:02:03.456789 DateTime64(6) +-- Commutativity +SELECT toTime('01:02:03') + toDate('2024-01-15') AS dt, toTypeName(dt); +2024-01-15 01:02:03 DateTime +SELECT toTime64('01:02:03.456', 3) + toDate('2024-01-15') AS dt, toTypeName(dt); +2024-01-15 01:02:03.456 DateTime64(3) +SELECT toTime('01:02:03') + toDate32('2024-01-15') AS dt, toTypeName(dt); +2024-01-15 01:02:03 DateTime64(0) +SELECT toTime64('01:02:03.456789', 6) + toDate32('2024-01-15') AS dt, toTypeName(dt); +2024-01-15 01:02:03.456789 DateTime64(6) +-- plus() functional syntax +SELECT plus(toDate('2024-01-15'), toTime('01:02:03')) AS dt, toTypeName(dt); +2024-01-15 01:02:03 DateTime +SELECT plus(toDate32('2024-01-15'), toTime64('01:02:03.456', 3)) AS dt, toTypeName(dt); +2024-01-15 01:02:03.456 DateTime64(3) +-- Time64(0) is intentionally different from Time (returns DateTime64(0) not DateTime) +SELECT toDate('2024-01-15') + toTime64('01:02:03', 0) AS dt, toTypeName(dt); +2024-01-15 01:02:03 DateTime64(0) +-- Date max works with Time64(0) because DateTime64(0) has larger range than DateTime +SELECT toDate('2149-06-06') + toTime64('23:59:59', 0) AS dt, toTypeName(dt); +2149-06-06 23:59:59 DateTime64(0) +-- But Date max with Time overflows because result type would be DateTime which has smaller range +SELECT toDate('2149-06-06') + toTime('23:59:59') AS dt, toTypeName(dt); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- Table-based tests +DROP TABLE IF EXISTS test_date_time64_0; +CREATE TABLE test_date_time64_0 (d Date, t Time64(0)) ENGINE = Memory; +INSERT INTO test_date_time64_0 VALUES + ('2024-01-15', '01:02:03'), + ('2149-06-06', '23:59:59'); +SELECT d + t AS dt, toTypeName(dt) FROM test_date_time64_0 ORDER BY d; +2024-01-15 01:02:03 DateTime64(0) +2149-06-06 23:59:59 DateTime64(0) +SELECT t + d AS dt, toTypeName(dt) FROM test_date_time64_0 ORDER BY d; +2024-01-15 01:02:03 DateTime64(0) +2149-06-06 23:59:59 DateTime64(0) +DROP TABLE test_date_time64_0; +DROP TABLE IF EXISTS test_date_time; +CREATE TABLE test_date_time (d Date, t Time) ENGINE = Memory; +INSERT INTO test_date_time VALUES ('2024-01-15', '01:02:03'), ('2024-06-20', '23:59:59'), ('1970-01-01', '00:00:00'); +SELECT d + t AS dt, toTypeName(dt) FROM test_date_time ORDER BY d; +1970-01-01 00:00:00 DateTime +2024-01-15 01:02:03 DateTime +2024-06-20 23:59:59 DateTime +DROP TABLE test_date_time; +DROP TABLE IF EXISTS test_date_time64; +CREATE TABLE test_date_time64 (d Date, t Time64(3)) ENGINE = Memory; +INSERT INTO test_date_time64 VALUES ('2024-01-15', '01:02:03.456'), ('2024-06-20', '23:59:59.999'), ('1970-01-01', '00:00:00.000'); +SELECT d + t AS dt, toTypeName(dt) FROM test_date_time64 ORDER BY d; +1970-01-01 00:00:00.000 DateTime64(3) +2024-01-15 01:02:03.456 DateTime64(3) +2024-06-20 23:59:59.999 DateTime64(3) +DROP TABLE test_date_time64; +DROP TABLE IF EXISTS test_date32_time; +CREATE TABLE test_date32_time (d Date32, t Time) ENGINE = Memory; +INSERT INTO test_date32_time VALUES ('2024-01-15', '01:02:03'), ('2200-06-15', '12:00:00'), ('1900-01-01', '23:59:59'); +SELECT d + t AS dt, toTypeName(dt) FROM test_date32_time ORDER BY d; +1900-01-01 23:59:59 DateTime64(0) +2024-01-15 01:02:03 DateTime64(0) +2200-06-15 12:00:00 DateTime64(0) +DROP TABLE test_date32_time; +DROP TABLE IF EXISTS test_date32_time64; +CREATE TABLE test_date32_time64 (d Date32, t Time64(3)) ENGINE = Memory; +INSERT INTO test_date32_time64 VALUES ('2024-01-15', '01:02:03.456'), ('2200-06-15', '12:00:00.000'), ('1900-01-01', '23:59:59.999'); +SELECT d + t AS dt, toTypeName(dt) FROM test_date32_time64 ORDER BY d; +1900-01-01 23:59:59.999 DateTime64(3) +2024-01-15 01:02:03.456 DateTime64(3) +2200-06-15 12:00:00.000 DateTime64(3) +DROP TABLE test_date32_time64; +-- Const/non-const column combinations + +-- Date + Time: const+const, const+vec, vec+const, vec+vec +SELECT toDate('2024-01-15') + toTime('01:02:03'); +2024-01-15 01:02:03 +SELECT toDate('2024-01-15') + materialize(toTime('01:02:03')); +2024-01-15 01:02:03 +SELECT materialize(toDate('2024-01-15')) + toTime('01:02:03'); +2024-01-15 01:02:03 +SELECT materialize(toDate('2024-01-15')) + materialize(toTime('01:02:03')); +2024-01-15 01:02:03 +-- Date + Time: table date column + const time +DROP TABLE IF EXISTS test_cv; +CREATE TABLE test_cv (d Date) ENGINE = Memory; +INSERT INTO test_cv VALUES ('2024-01-15'), ('2024-06-20'), ('1970-01-01'); +SELECT d + toTime('01:02:03') FROM test_cv ORDER BY d; +1970-01-01 01:02:03 +2024-01-15 01:02:03 +2024-06-20 01:02:03 +-- Date + Time: const date + table time column +DROP TABLE IF EXISTS test_cv2; +CREATE TABLE test_cv2 (t Time) ENGINE = Memory; +INSERT INTO test_cv2 VALUES ('01:02:03'), ('23:59:59'), ('00:00:00'); +SELECT toDate('2024-01-15') + t FROM test_cv2 ORDER BY t; +2024-01-15 00:00:00 +2024-01-15 01:02:03 +2024-01-15 23:59:59 +DROP TABLE test_cv; +DROP TABLE test_cv2; +-- Date + Time64: table date column + const time64, const date + table time64 column +DROP TABLE IF EXISTS test_cv3; +CREATE TABLE test_cv3 (d Date, t Time64(3)) ENGINE = Memory; +INSERT INTO test_cv3 VALUES ('2024-01-15', '01:02:03.100'), ('2024-06-20', '23:59:59.999'); +SELECT d + toTime64('12:00:00.000', 3) FROM test_cv3 ORDER BY d; +2024-01-15 12:00:00.000 +2024-06-20 12:00:00.000 +SELECT toDate('2024-01-15') + t FROM test_cv3 ORDER BY t; +2024-01-15 01:02:03.100 +2024-01-15 23:59:59.999 +SELECT d + t FROM test_cv3 ORDER BY d; +2024-01-15 01:02:03.100 +2024-06-20 23:59:59.999 +DROP TABLE test_cv3; +-- Date32 + Time: table date32 column + const time, const date32 + table time column +DROP TABLE IF EXISTS test_cv4; +CREATE TABLE test_cv4 (d Date32, t Time) ENGINE = Memory; +INSERT INTO test_cv4 VALUES ('2024-01-15', '01:02:03'), ('2200-06-15', '12:00:00'); +SELECT d + toTime('06:00:00') FROM test_cv4 ORDER BY d; +2024-01-15 06:00:00 +2200-06-15 06:00:00 +SELECT toDate32('2024-01-15') + t FROM test_cv4 ORDER BY t; +2024-01-15 01:02:03 +2024-01-15 12:00:00 +SELECT d + t FROM test_cv4 ORDER BY d; +2024-01-15 01:02:03 +2200-06-15 12:00:00 +DROP TABLE test_cv4; +-- Date32 + Time64: table date32 column + const time64, const date32 + table time64 column +DROP TABLE IF EXISTS test_cv5; +CREATE TABLE test_cv5 (d Date32, t Time64(6)) ENGINE = Memory; +INSERT INTO test_cv5 VALUES ('2024-01-15', '01:02:03.123456'), ('1900-01-01', '23:59:59.999999'); +SELECT d + toTime64('06:00:00.000000', 6) FROM test_cv5 ORDER BY d; +1900-01-01 06:00:00.000000 +2024-01-15 06:00:00.000000 +SELECT toDate32('2024-01-15') + t FROM test_cv5 ORDER BY t; +2024-01-15 01:02:03.123456 +2024-01-15 23:59:59.999999 +SELECT d + t FROM test_cv5 ORDER BY d; +1900-01-01 23:59:59.999999 +2024-01-15 01:02:03.123456 +DROP TABLE test_cv5; +-- Edge cases: Date + Time -> DateTime + +-- Epoch boundaries +SELECT toDate('1970-01-01') + toTime('00:00:00'); +1970-01-01 00:00:00 +SELECT toDate('1970-01-01') + toTime('23:59:59'); +1970-01-01 23:59:59 +-- End of day +SELECT toDate('2024-01-15') + toTime('23:59:59'); +2024-01-15 23:59:59 +-- Negative time -> wraps to previous day +SELECT toDate('2024-01-15') + toTime(-1); +2024-01-14 23:59:59 +-- Time exceeding 24h -> wraps into next day +SELECT toDate('2024-01-15') + toTime(90000); +2024-01-16 01:00:00 +-- Near DateTime max boundary +SELECT toDate('2106-02-07') + toTime('00:00:00'); +2106-02-07 00:00:00 +SELECT toDate('2106-02-07') + toTime('06:28:15'); +2106-02-07 06:28:15 +-- Edge cases: Date + Time64 -> DateTime64 + +-- Epoch boundaries +SELECT toDate('1970-01-01') + toTime64('00:00:00.000', 3); +1970-01-01 00:00:00.000 +SELECT toDate('1970-01-01') + toTime64('23:59:59.999', 3); +1970-01-01 23:59:59.999 +-- End of day with max scale +SELECT toDate('2024-01-15') + toTime64('23:59:59.999999999', 9) AS dt, toTypeName(dt); +2024-01-15 23:59:59.999999999 DateTime64(9) +-- Negative time +SELECT toDate('2024-01-15') + toTime64(-1, 3); +2024-01-14 23:59:59.000 +-- Negative time from epoch (DateTime64 supports pre-epoch) +SELECT toDate('1970-01-01') + toTime64(-1, 3); +1969-12-31 23:59:59.000 +-- Time exceeding 24h +SELECT toDate('2024-01-15') + toTime64(90000, 3); +2024-01-16 01:00:00.000 +-- Near boundary dates +SELECT toDate('2106-02-07') + toTime64('06:28:15.000', 3); +2106-02-07 06:28:15.000 +-- Date max is in range for DateTime64(3) +SELECT toDate('2149-06-06') + toTime64('23:59:59.999', 3); +2149-06-06 23:59:59.999 +-- Edge cases: Date32 + Time -> DateTime64(0) + +-- Epoch boundaries +SELECT toDate32('1970-01-01') + toTime('00:00:00'); +1970-01-01 00:00:00 +SELECT toDate32('1970-01-01') + toTime('23:59:59'); +1970-01-01 23:59:59 +-- Pre-epoch +SELECT toDate32('1900-01-01') + toTime('00:00:00'); +1900-01-01 00:00:00 +SELECT toDate32('1900-01-01') + toTime('23:59:59'); +1900-01-01 23:59:59 +-- Negative time +SELECT toDate32('2024-01-15') + toTime(-1); +2024-01-14 23:59:59 +-- Time exceeding 24h +SELECT toDate32('2024-01-15') + toTime(90000); +2024-01-16 01:00:00 +-- Beyond DateTime range (DateTime64 handles it) +SELECT toDate32('2200-06-15') + toTime('12:00:00'); +2200-06-15 12:00:00 +SELECT toDate32('2200-06-15') + toTime('23:59:59'); +2200-06-15 23:59:59 +-- Date32 max supported range +SELECT toDate32('2299-12-31') + toTime('23:59:59'); +2299-12-31 23:59:59 +-- Edge cases: Date32 + Time64 -> DateTime64(s) + +-- Epoch boundaries +SELECT toDate32('1970-01-01') + toTime64('00:00:00.000', 3); +1970-01-01 00:00:00.000 +SELECT toDate32('1970-01-01') + toTime64('23:59:59.999', 3); +1970-01-01 23:59:59.999 +-- Pre-epoch +SELECT toDate32('1900-01-01') + toTime64('00:00:00.000', 3); +1900-01-01 00:00:00.000 +SELECT toDate32('1900-01-01') + toTime64('23:59:59.999', 3); +1900-01-01 23:59:59.999 +-- Negative time +SELECT toDate32('2024-01-15') + toTime64(-1, 3); +2024-01-14 23:59:59.000 +-- Time exceeding 24h +SELECT toDate32('2024-01-15') + toTime64(90000, 3); +2024-01-16 01:00:00.000 +-- Beyond DateTime range +SELECT toDate32('2200-06-15') + toTime64('12:30:45.678', 3); +2200-06-15 12:30:45.678 +-- Normalization and multi-day rollover (scalar) + +SELECT toDate('2024-01-15') + toTime('25:70:70'); +2024-01-16 02:11:10 +SELECT toDate32('2024-01-15') + toTime64('25:70:70.123456', 6); +2024-01-16 02:11:10.123456 +-- Maximum visible Time range +SELECT toDate('2024-01-15') + toTime('999:59:59'); +2024-02-25 15:59:59 +-- Sub-second negative and overflow for Time64 +SELECT toDate('2024-01-15') + toTime64('-00:00:00.001', 3); +2024-01-14 23:59:59.999 +SELECT toDate('2024-01-15') + toTime64('24:00:00.001', 3); +2024-01-16 00:00:00.001 +-- Date + Time with negative / >24h / normalized values +DROP TABLE IF EXISTS test_roll_time; +CREATE TABLE test_roll_time (d Date, t Time) ENGINE = Memory; +INSERT INTO test_roll_time VALUES + ('2024-01-15', -1), + ('2024-01-15', 90000), + ('2024-01-15', '25:70:70'); +SELECT d + t AS dt, toTypeName(dt) FROM test_roll_time ORDER BY t; +2024-01-14 23:59:59 DateTime +2024-01-16 01:00:00 DateTime +2024-01-16 02:11:10 DateTime +DROP TABLE test_roll_time; +-- Date32 + Time64 with negative / >24h / normalized values +DROP TABLE IF EXISTS test_roll_time64; +CREATE TABLE test_roll_time64 (d Date32, t Time64(6)) ENGINE = Memory; +INSERT INTO test_roll_time64 VALUES + ('2024-01-15', '-00:00:00.001000'), + ('2024-01-15', '24:00:00.001000'), + ('2024-01-15', '25:70:70.123456'); +SELECT d + t AS dt, toTypeName(dt) FROM test_roll_time64 ORDER BY t; +2024-01-14 23:59:59.999000 DateTime64(6) +2024-01-16 00:00:00.001000 DateTime64(6) +2024-01-16 02:11:10.123456 DateTime64(6) +DROP TABLE test_roll_time64; +-- Scale 8 fits the full DateTime64 range (1900-2299), but scale 9 is limited to ~2262 by Int64 capacity +-- Precision 8 still reaches full Date32/DateTime64 range +SELECT toDate32('2299-12-31') + toTime64('23:59:59.99999999', 8) AS dt, toTypeName(dt); +2299-12-31 23:59:59.99999999 DateTime64(8) +-- Precision 9 is still fully safe for all Date values, because Date tops out before 2262 +SELECT toDate('2149-06-06') + toTime64('23:59:59.999999999', 9) AS dt, toTypeName(dt); +2149-06-06 23:59:59.999999999 DateTime64(9) +-- For in-range values, Date + Time matches Date + INTERVAL +-- (overflow behavior may differ: Date+Time respects date_time_overflow_behavior, INTERVAL does not) +SELECT (toDate('2024-01-15') + toTime(3723)) = (toDate('2024-01-15') + INTERVAL 3723 SECOND); +1 +-- 24h rollover: Date + Time(86400) vs Date + INTERVAL 86400 SECOND +SELECT (toDate('2024-01-15') + toTime(86400)) = (toDate('2024-01-15') + INTERVAL 86400 SECOND); +1 +-- Negative time: Date + Time(-1) vs Date + INTERVAL -1 SECOND +SELECT (toDate('2024-01-15') + toTime(-1)) = (toDate('2024-01-15') + INTERVAL -1 SECOND); +1 +-- Date32 +SELECT (toDate32('2024-01-15') + toTime(3723)) = (toDate32('2024-01-15') + INTERVAL 3723 SECOND); +1 +-- Timezone handling + +SET session_timezone = 'UTC'; +SELECT toDate('2024-01-15') + toTime('01:02:03') AS dtest_utc; +2024-01-15 01:02:03 +SELECT toUnixTimestamp(toDate('2024-01-15') + toTime(0)) AS ts_utc; +1705276800 +-- Verify underlying millisecond timestamp is correct under UTC +SELECT toUnixTimestamp64Milli(toDate('2024-01-15') + toTime64('00:00:00.123', 3)) AS ts64_utc; +1705276800123 +-- Side-by-side with INTERVAL under UTC +SELECT (toDate('2024-01-15') + toTime(3723)) = (toDate('2024-01-15') + INTERVAL 3723 SECOND) AS same_utc; +1 +SET session_timezone = 'America/New_York'; +SELECT toDate('2024-01-15') + toTime('01:02:03') AS dtest_ny; +2024-01-15 01:02:03 +SELECT toUnixTimestamp(toDate('2024-01-15') + toTime(0)) AS ts_ny; +1705294800 +-- Verify underlying millisecond timestamp differs under New York timezone +SELECT toUnixTimestamp64Milli(toDate('2024-01-15') + toTime64('00:00:00.123', 3)) AS ts64_ny; +1705294800123 +-- Side-by-side with INTERVAL under New York timezone +SELECT (toDate('2024-01-15') + toTime(3723)) = (toDate('2024-01-15') + INTERVAL 3723 SECOND) AS same_ny; +1 +SET session_timezone = 'Asia/Kolkata'; +SELECT toDate('2024-01-15') + toTime('01:02:03') AS dtest_kol; +2024-01-15 01:02:03 +SELECT toUnixTimestamp(toDate('2024-01-15') + toTime(0)) AS ts_kol; +1705257000 +-- Side-by-side with INTERVAL under Kolkata timezone +SELECT (toDate('2024-01-15') + toTime(3723)) = (toDate('2024-01-15') + INTERVAL 3723 SECOND) AS same_kol; +1 +-- Verify timestamps differ between timezones (proving tz affects computation) +SET session_timezone = 'UTC'; +SELECT + toUnixTimestamp(toDateTime('2024-01-15 00:00:00', 'UTC')) AS ts_utc, + toUnixTimestamp(toDateTime('2024-01-15 00:00:00', 'America/New_York')) AS ts_ny, + ts_utc != ts_ny AS differ; +1705276800 1705294800 1 +-- DST: Date+Time adds raw seconds to midnight, which can differ from parsing a +-- local timestamp string when DST transitions create gaps or overlaps. + +SET session_timezone = 'Europe/London'; +-- 2023-10-29: London clocks fall back from 02:00 to 01:00 (01:30 occurs twice). +-- Date+Time gives midnight+5400s. toDateTime parses '01:30' picking the earlier occurrence. +-- Both resolve to the same instant, so the result is equal. +SELECT (toDate('2023-10-29') + toTime('01:30:00')) = toDateTime('2023-10-29 01:30:00', 'Europe/London'); +1 +-- 2023-03-26: London clocks spring forward from 01:00 to 02:00 (01:30 does not exist). +-- Date+Time gives midnight+5400s = 02:30 BST (the gap is skipped arithmetically). +-- toDateTime parses nonexistent '01:30' as 00:30 GMT (shifts back before the gap). +-- These are different instants, so the result is not equal. +SELECT (toDate('2023-03-26') + toTime('01:30:00')) = toDateTime('2023-03-26 01:30:00', 'Europe/London'); +0 +SELECT (toDate('2023-03-26') + toTime64('01:30:00.123', 3)) = toDateTime64('2023-03-26 01:30:00.123', 3, 'Europe/London'); +0 +-- Show the actual values: 02:30 vs 00:30, different unix timestamps +SELECT + toDate('2023-03-26') + toTime('01:30:00') AS combined, + toDateTime('2023-03-26 01:30:00', 'Europe/London') AS parsed; +2023-03-26 02:30:00 2023-03-26 00:30:00 +SELECT + toUnixTimestamp(toDate('2023-03-26') + toTime('01:30:00')) AS combined_ts, + toUnixTimestamp(toDateTime('2023-03-26 01:30:00', 'Europe/London')) AS parsed_ts; +1679794200 1679790600 +-- Date+Time matches midnight+INTERVAL (both just add raw seconds to midnight) +SELECT + (toDate('2023-03-26') + toTime('01:30:00')) = + (toDateTime('2023-03-26 00:00:00', 'Europe/London') + INTERVAL 5400 SECOND); +1 +SET session_timezone = 'UTC'; +-- Common-type test: Date+Time and Date+Time64(0) should unify to DateTime64(0) +SELECT DISTINCT toTypeName(dt) FROM +( + SELECT toDate('2024-01-15') + toTime('01:02:03') AS dt + UNION ALL + SELECT toDate('2024-01-15') + toTime64('01:02:03', 0) AS dt +); +DateTime64(0) +-- Error cases + +SELECT toDate('2024-01-15') - toTime('01:02:03'); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toTime('01:02:03') - toDate('2024-01-15'); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toDateTime('2024-01-15 00:00:00') + toTime('01:02:03'); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toDate('2024-01-15') - toTime64('01:02:03.456', 3); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toDateTime('2024-01-15 00:00:00') + toTime64('01:02:03.456', 3); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toDateTime64('2024-01-15 00:00:00.000', 3) + toTime('01:02:03'); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toDateTime64('2024-01-15 00:00:00.000', 3) + toTime64('01:02:03.456', 3); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +-- Overflow with throw (already the default from top of file) + +-- Date + Time -> DateTime: underflow below epoch +SELECT toDate('1970-01-01') + toTime(-1); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- Date + Time -> DateTime: max valid DateTime +SELECT toDate('2106-02-07') + toTime('06:28:15'); +2106-02-07 06:28:15 +-- Date + Time -> DateTime: one second past max +SELECT toDate('2106-02-07') + toTime('06:28:16'); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- Date + Time -> DateTime: Date max far exceeds DateTime range +SELECT toDate('2149-06-06') + toTime('00:00:00'); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- Date32 + Time -> DateTime64(0): underflow below 1900 +SELECT toDate32('1900-01-01') + toTime(-1); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- Date32 + Time64 -> DateTime64: underflow below 1900 +SELECT toDate32('1900-01-01') + toTime64('-00:00:00.000001', 6); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- DateTime64(9) specific boundary: lower limit (1900-01-01 midnight) +SELECT toDate32('1900-01-01') + toTime64('00:00:00.000000000', 9) AS dt, toTypeName(dt); +1900-01-01 00:00:00.000000000 DateTime64(9) +-- DateTime64(9): a value on the last valid day (exact upper limit is 23:47:16.854775807 below) +SELECT toDate32('2262-04-11') + toTime64('23:47:16.000000000', 9) AS dt, toTypeName(dt); +2262-04-11 23:47:16.000000000 DateTime64(9) +-- DateTime64(9) specific boundary: past Int64 capacity at scale 9 +SELECT toDate32('2262-04-12') + toTime64('00:00:00.000000000', 9); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- One tick past the last representable DateTime64(9) value +SELECT toDate32('2262-04-11') + toTime64('23:47:16.854775808', 9); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- Last representable tick at scale 9 +SELECT toDate32('2262-04-11') + toTime64('23:47:16.854775807', 9) AS dt, toTypeName(dt); +2262-04-11 23:47:16.854775807 DateTime64(9) +-- Vector path: one row valid, one row past DateTime64(9) limit +DROP TABLE IF EXISTS test_overflow_int64; +CREATE TABLE test_overflow_int64 (d Date32, t Time64(9)) ENGINE = Memory; +INSERT INTO test_overflow_int64 VALUES + ('2262-04-11', '23:47:16.854775807'), + ('2262-04-11', '23:47:16.854775808'); +SELECT d + t FROM test_overflow_int64 ORDER BY t; -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +DROP TABLE test_overflow_int64; +-- Vector overflow regression: throw must work on non-const columns too +DROP TABLE IF EXISTS test_overflow_vec; +CREATE TABLE test_overflow_vec (d Date, t Time) ENGINE = Memory; +INSERT INTO test_overflow_vec VALUES + ('2106-02-07', '06:28:15'), + ('2149-06-06', '00:00:00'); +SELECT d + t FROM test_overflow_vec ORDER BY d; -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +DROP TABLE test_overflow_vec; +-- Vector path: Date32 + Time64(9) with one row past the DateTime64(9) limit +DROP TABLE IF EXISTS test_overflow_vec64; +CREATE TABLE test_overflow_vec64 (d Date32, t Time64(9)) ENGINE = Memory; +INSERT INTO test_overflow_vec64 VALUES + ('2262-04-11', '23:47:16.000000000'), + ('2262-04-12', '00:00:00.000000000'); +SELECT d + t FROM test_overflow_vec64 ORDER BY d; -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +DROP TABLE test_overflow_vec64; +-- Large negative Time64 can bring an intermediate overflow back into range. +-- midnight(2299-12-31) * 10^9 overflows Int64, but subtracting 1.2B seconds lands in range. +SELECT toDate32('2299-12-31') + CAST(toDecimal128('-1200000000.000000000', 9), 'Time64(9)'); +2261-12-21 02:40:00.000000000 +-- Vector path: one row has large negative time (in range), other row overflows. +DROP TABLE IF EXISTS test_intermediate_overflow; +CREATE TABLE test_intermediate_overflow (t Time64(9)) ENGINE = Memory; +INSERT INTO test_intermediate_overflow SELECT arrayJoin([CAST(toDecimal128('-1200000000.000000000', 9), 'Time64(9)'), toTime64('00:00:00.000000000', 9)]); +SELECT toDate32('2299-12-31') + t FROM test_intermediate_overflow ORDER BY t; -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +DROP TABLE test_intermediate_overflow; +-- Overflow with saturate + +SET date_time_overflow_behavior = 'saturate'; +-- Date + Time: underflow saturates to epoch +SELECT toDate('1970-01-01') + toTime(-1); +1970-01-01 00:00:00 +-- Date + Time: overflow saturates to DateTime max +SELECT toDate('2149-06-06') + toTime('23:59:59'); +2106-02-07 06:28:15 +-- Date32 + Time64: underflow saturates to DateTime64 min +SELECT toDate32('1900-01-01') + toTime64('-00:00:00.000001', 6); +1900-01-01 00:00:00.000000 +-- DateTime64(9) overflow saturates to the last representable value +SELECT toDate32('2262-04-11') + toTime64('23:47:16.854775808', 9); +2262-04-11 23:47:16.854775807 +-- Large negative time brings intermediate overflow back into range (should NOT saturate) +SELECT toDate32('2299-12-31') + CAST(toDecimal128('-1200000000.000000000', 9), 'Time64(9)'); +2261-12-21 02:40:00.000000000 +-- Vector: first row is in range despite intermediate overflow, second row saturates +DROP TABLE IF EXISTS test_saturate_intermediate; +CREATE TABLE test_saturate_intermediate (t Time64(9)) ENGINE = Memory; +INSERT INTO test_saturate_intermediate SELECT arrayJoin([CAST(toDecimal128('-1200000000.000000000', 9), 'Time64(9)'), toTime64('00:00:00.000000000', 9)]); +SELECT toDate32('2299-12-31') + t FROM test_saturate_intermediate ORDER BY t; +2261-12-21 02:40:00.000000000 +2262-04-11 23:47:16.854775807 +DROP TABLE test_saturate_intermediate; +SET date_time_overflow_behavior = 'throw'; +-- Time values beyond the visible range display as saturated (999:59:59 or -999:59:59 +-- depending on sign) but internally store their full numeric value. Date+Time uses +-- the internal value, so two Time values that print identically can produce different +-- DateTime results. +SELECT + toTime(9999999) AS t_raw, + toTime(3599999) AS t_vis, + t_raw = t_vis AS same_time; +999:59:59 999:59:59 0 +SELECT + toDate('2024-01-15') + toTime(9999999) AS dt_raw, + toDate('2024-01-15') + toTime(3599999) AS dt_vis, + dt_raw = dt_vis AS same_dt; +2024-05-09 17:46:39 2024-02-25 15:59:59 0 +SELECT + (toDate('2024-01-15') + toTime(9999999)) = + (toDate('2024-01-15') + toTime(3599999)) AS same_dt_from_same_visible_time; +0 diff --git a/tests/queries/0_stateless/04092_date_plus_time.sql b/tests/queries/0_stateless/04092_date_plus_time.sql new file mode 100644 index 000000000000..46878ff5c0ed --- /dev/null +++ b/tests/queries/0_stateless/04092_date_plus_time.sql @@ -0,0 +1,420 @@ +-- { echo } + +SET enable_time_time64_type = 1; +SET use_legacy_to_time = 0; +SET session_timezone = 'UTC'; +SET date_time_overflow_behavior = 'throw'; + +-- Date + Time -> DateTime +SELECT toDate('2024-01-15') + toTime('01:02:03') AS dt, toTypeName(dt); + +-- Date + Time64 -> DateTime64 +SELECT toDate('2024-01-15') + toTime64('01:02:03.456', 3) AS dt, toTypeName(dt); + +-- Date32 + Time -> DateTime64(0) +SELECT toDate32('2024-01-15') + toTime('01:02:03') AS dt, toTypeName(dt); + +-- Date32 + Time64 -> DateTime64(6) +SELECT toDate32('2024-01-15') + toTime64('01:02:03.456789', 6) AS dt, toTypeName(dt); + +-- Commutativity +SELECT toTime('01:02:03') + toDate('2024-01-15') AS dt, toTypeName(dt); +SELECT toTime64('01:02:03.456', 3) + toDate('2024-01-15') AS dt, toTypeName(dt); +SELECT toTime('01:02:03') + toDate32('2024-01-15') AS dt, toTypeName(dt); +SELECT toTime64('01:02:03.456789', 6) + toDate32('2024-01-15') AS dt, toTypeName(dt); + +-- plus() functional syntax +SELECT plus(toDate('2024-01-15'), toTime('01:02:03')) AS dt, toTypeName(dt); +SELECT plus(toDate32('2024-01-15'), toTime64('01:02:03.456', 3)) AS dt, toTypeName(dt); + +-- Time64(0) is intentionally different from Time (returns DateTime64(0) not DateTime) +SELECT toDate('2024-01-15') + toTime64('01:02:03', 0) AS dt, toTypeName(dt); +-- Date max works with Time64(0) because DateTime64(0) has larger range than DateTime +SELECT toDate('2149-06-06') + toTime64('23:59:59', 0) AS dt, toTypeName(dt); +-- But Date max with Time overflows because result type would be DateTime which has smaller range +SELECT toDate('2149-06-06') + toTime('23:59:59') AS dt, toTypeName(dt); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } + +-- Table-based tests +DROP TABLE IF EXISTS test_date_time64_0; +CREATE TABLE test_date_time64_0 (d Date, t Time64(0)) ENGINE = Memory; +INSERT INTO test_date_time64_0 VALUES + ('2024-01-15', '01:02:03'), + ('2149-06-06', '23:59:59'); +SELECT d + t AS dt, toTypeName(dt) FROM test_date_time64_0 ORDER BY d; +SELECT t + d AS dt, toTypeName(dt) FROM test_date_time64_0 ORDER BY d; +DROP TABLE test_date_time64_0; + +DROP TABLE IF EXISTS test_date_time; +CREATE TABLE test_date_time (d Date, t Time) ENGINE = Memory; +INSERT INTO test_date_time VALUES ('2024-01-15', '01:02:03'), ('2024-06-20', '23:59:59'), ('1970-01-01', '00:00:00'); +SELECT d + t AS dt, toTypeName(dt) FROM test_date_time ORDER BY d; +DROP TABLE test_date_time; + +DROP TABLE IF EXISTS test_date_time64; +CREATE TABLE test_date_time64 (d Date, t Time64(3)) ENGINE = Memory; +INSERT INTO test_date_time64 VALUES ('2024-01-15', '01:02:03.456'), ('2024-06-20', '23:59:59.999'), ('1970-01-01', '00:00:00.000'); +SELECT d + t AS dt, toTypeName(dt) FROM test_date_time64 ORDER BY d; +DROP TABLE test_date_time64; + +DROP TABLE IF EXISTS test_date32_time; +CREATE TABLE test_date32_time (d Date32, t Time) ENGINE = Memory; +INSERT INTO test_date32_time VALUES ('2024-01-15', '01:02:03'), ('2200-06-15', '12:00:00'), ('1900-01-01', '23:59:59'); +SELECT d + t AS dt, toTypeName(dt) FROM test_date32_time ORDER BY d; +DROP TABLE test_date32_time; + +DROP TABLE IF EXISTS test_date32_time64; +CREATE TABLE test_date32_time64 (d Date32, t Time64(3)) ENGINE = Memory; +INSERT INTO test_date32_time64 VALUES ('2024-01-15', '01:02:03.456'), ('2200-06-15', '12:00:00.000'), ('1900-01-01', '23:59:59.999'); +SELECT d + t AS dt, toTypeName(dt) FROM test_date32_time64 ORDER BY d; +DROP TABLE test_date32_time64; + +-- Const/non-const column combinations + +-- Date + Time: const+const, const+vec, vec+const, vec+vec +SELECT toDate('2024-01-15') + toTime('01:02:03'); +SELECT toDate('2024-01-15') + materialize(toTime('01:02:03')); +SELECT materialize(toDate('2024-01-15')) + toTime('01:02:03'); +SELECT materialize(toDate('2024-01-15')) + materialize(toTime('01:02:03')); + +-- Date + Time: table date column + const time +DROP TABLE IF EXISTS test_cv; +CREATE TABLE test_cv (d Date) ENGINE = Memory; +INSERT INTO test_cv VALUES ('2024-01-15'), ('2024-06-20'), ('1970-01-01'); +SELECT d + toTime('01:02:03') FROM test_cv ORDER BY d; +-- Date + Time: const date + table time column +DROP TABLE IF EXISTS test_cv2; +CREATE TABLE test_cv2 (t Time) ENGINE = Memory; +INSERT INTO test_cv2 VALUES ('01:02:03'), ('23:59:59'), ('00:00:00'); +SELECT toDate('2024-01-15') + t FROM test_cv2 ORDER BY t; +DROP TABLE test_cv; +DROP TABLE test_cv2; + +-- Date + Time64: table date column + const time64, const date + table time64 column +DROP TABLE IF EXISTS test_cv3; +CREATE TABLE test_cv3 (d Date, t Time64(3)) ENGINE = Memory; +INSERT INTO test_cv3 VALUES ('2024-01-15', '01:02:03.100'), ('2024-06-20', '23:59:59.999'); +SELECT d + toTime64('12:00:00.000', 3) FROM test_cv3 ORDER BY d; +SELECT toDate('2024-01-15') + t FROM test_cv3 ORDER BY t; +SELECT d + t FROM test_cv3 ORDER BY d; +DROP TABLE test_cv3; + +-- Date32 + Time: table date32 column + const time, const date32 + table time column +DROP TABLE IF EXISTS test_cv4; +CREATE TABLE test_cv4 (d Date32, t Time) ENGINE = Memory; +INSERT INTO test_cv4 VALUES ('2024-01-15', '01:02:03'), ('2200-06-15', '12:00:00'); +SELECT d + toTime('06:00:00') FROM test_cv4 ORDER BY d; +SELECT toDate32('2024-01-15') + t FROM test_cv4 ORDER BY t; +SELECT d + t FROM test_cv4 ORDER BY d; +DROP TABLE test_cv4; + +-- Date32 + Time64: table date32 column + const time64, const date32 + table time64 column +DROP TABLE IF EXISTS test_cv5; +CREATE TABLE test_cv5 (d Date32, t Time64(6)) ENGINE = Memory; +INSERT INTO test_cv5 VALUES ('2024-01-15', '01:02:03.123456'), ('1900-01-01', '23:59:59.999999'); +SELECT d + toTime64('06:00:00.000000', 6) FROM test_cv5 ORDER BY d; +SELECT toDate32('2024-01-15') + t FROM test_cv5 ORDER BY t; +SELECT d + t FROM test_cv5 ORDER BY d; +DROP TABLE test_cv5; + +-- Edge cases: Date + Time -> DateTime + +-- Epoch boundaries +SELECT toDate('1970-01-01') + toTime('00:00:00'); +SELECT toDate('1970-01-01') + toTime('23:59:59'); +-- End of day +SELECT toDate('2024-01-15') + toTime('23:59:59'); +-- Negative time -> wraps to previous day +SELECT toDate('2024-01-15') + toTime(-1); +-- Time exceeding 24h -> wraps into next day +SELECT toDate('2024-01-15') + toTime(90000); +-- Near DateTime max boundary +SELECT toDate('2106-02-07') + toTime('00:00:00'); +SELECT toDate('2106-02-07') + toTime('06:28:15'); + +-- Edge cases: Date + Time64 -> DateTime64 + +-- Epoch boundaries +SELECT toDate('1970-01-01') + toTime64('00:00:00.000', 3); +SELECT toDate('1970-01-01') + toTime64('23:59:59.999', 3); +-- End of day with max scale +SELECT toDate('2024-01-15') + toTime64('23:59:59.999999999', 9) AS dt, toTypeName(dt); +-- Negative time +SELECT toDate('2024-01-15') + toTime64(-1, 3); +-- Negative time from epoch (DateTime64 supports pre-epoch) +SELECT toDate('1970-01-01') + toTime64(-1, 3); +-- Time exceeding 24h +SELECT toDate('2024-01-15') + toTime64(90000, 3); +-- Near boundary dates +SELECT toDate('2106-02-07') + toTime64('06:28:15.000', 3); +-- Date max is in range for DateTime64(3) +SELECT toDate('2149-06-06') + toTime64('23:59:59.999', 3); + +-- Edge cases: Date32 + Time -> DateTime64(0) + +-- Epoch boundaries +SELECT toDate32('1970-01-01') + toTime('00:00:00'); +SELECT toDate32('1970-01-01') + toTime('23:59:59'); +-- Pre-epoch +SELECT toDate32('1900-01-01') + toTime('00:00:00'); +SELECT toDate32('1900-01-01') + toTime('23:59:59'); +-- Negative time +SELECT toDate32('2024-01-15') + toTime(-1); +-- Time exceeding 24h +SELECT toDate32('2024-01-15') + toTime(90000); +-- Beyond DateTime range (DateTime64 handles it) +SELECT toDate32('2200-06-15') + toTime('12:00:00'); +SELECT toDate32('2200-06-15') + toTime('23:59:59'); +-- Date32 max supported range +SELECT toDate32('2299-12-31') + toTime('23:59:59'); + +-- Edge cases: Date32 + Time64 -> DateTime64(s) + +-- Epoch boundaries +SELECT toDate32('1970-01-01') + toTime64('00:00:00.000', 3); +SELECT toDate32('1970-01-01') + toTime64('23:59:59.999', 3); +-- Pre-epoch +SELECT toDate32('1900-01-01') + toTime64('00:00:00.000', 3); +SELECT toDate32('1900-01-01') + toTime64('23:59:59.999', 3); +-- Negative time +SELECT toDate32('2024-01-15') + toTime64(-1, 3); +-- Time exceeding 24h +SELECT toDate32('2024-01-15') + toTime64(90000, 3); +-- Beyond DateTime range +SELECT toDate32('2200-06-15') + toTime64('12:30:45.678', 3); + +-- Normalization and multi-day rollover (scalar) + +SELECT toDate('2024-01-15') + toTime('25:70:70'); +SELECT toDate32('2024-01-15') + toTime64('25:70:70.123456', 6); +-- Maximum visible Time range +SELECT toDate('2024-01-15') + toTime('999:59:59'); +-- Sub-second negative and overflow for Time64 +SELECT toDate('2024-01-15') + toTime64('-00:00:00.001', 3); +SELECT toDate('2024-01-15') + toTime64('24:00:00.001', 3); + +-- Date + Time with negative / >24h / normalized values +DROP TABLE IF EXISTS test_roll_time; +CREATE TABLE test_roll_time (d Date, t Time) ENGINE = Memory; +INSERT INTO test_roll_time VALUES + ('2024-01-15', -1), + ('2024-01-15', 90000), + ('2024-01-15', '25:70:70'); +SELECT d + t AS dt, toTypeName(dt) FROM test_roll_time ORDER BY t; +DROP TABLE test_roll_time; + +-- Date32 + Time64 with negative / >24h / normalized values +DROP TABLE IF EXISTS test_roll_time64; +CREATE TABLE test_roll_time64 (d Date32, t Time64(6)) ENGINE = Memory; +INSERT INTO test_roll_time64 VALUES + ('2024-01-15', '-00:00:00.001000'), + ('2024-01-15', '24:00:00.001000'), + ('2024-01-15', '25:70:70.123456'); +SELECT d + t AS dt, toTypeName(dt) FROM test_roll_time64 ORDER BY t; +DROP TABLE test_roll_time64; + +-- Scale 8 fits the full DateTime64 range (1900-2299), but scale 9 is limited to ~2262 by Int64 capacity +-- Precision 8 still reaches full Date32/DateTime64 range +SELECT toDate32('2299-12-31') + toTime64('23:59:59.99999999', 8) AS dt, toTypeName(dt); +-- Precision 9 is still fully safe for all Date values, because Date tops out before 2262 +SELECT toDate('2149-06-06') + toTime64('23:59:59.999999999', 9) AS dt, toTypeName(dt); + +-- For in-range values, Date + Time matches Date + INTERVAL +-- (overflow behavior may differ: Date+Time respects date_time_overflow_behavior, INTERVAL does not) +SELECT (toDate('2024-01-15') + toTime(3723)) = (toDate('2024-01-15') + INTERVAL 3723 SECOND); +-- 24h rollover: Date + Time(86400) vs Date + INTERVAL 86400 SECOND +SELECT (toDate('2024-01-15') + toTime(86400)) = (toDate('2024-01-15') + INTERVAL 86400 SECOND); +-- Negative time: Date + Time(-1) vs Date + INTERVAL -1 SECOND +SELECT (toDate('2024-01-15') + toTime(-1)) = (toDate('2024-01-15') + INTERVAL -1 SECOND); +-- Date32 +SELECT (toDate32('2024-01-15') + toTime(3723)) = (toDate32('2024-01-15') + INTERVAL 3723 SECOND); + +-- Timezone handling + +SET session_timezone = 'UTC'; +SELECT toDate('2024-01-15') + toTime('01:02:03') AS dtest_utc; +SELECT toUnixTimestamp(toDate('2024-01-15') + toTime(0)) AS ts_utc; +-- Verify underlying millisecond timestamp is correct under UTC +SELECT toUnixTimestamp64Milli(toDate('2024-01-15') + toTime64('00:00:00.123', 3)) AS ts64_utc; + +-- Side-by-side with INTERVAL under UTC +SELECT (toDate('2024-01-15') + toTime(3723)) = (toDate('2024-01-15') + INTERVAL 3723 SECOND) AS same_utc; + +SET session_timezone = 'America/New_York'; +SELECT toDate('2024-01-15') + toTime('01:02:03') AS dtest_ny; +SELECT toUnixTimestamp(toDate('2024-01-15') + toTime(0)) AS ts_ny; +-- Verify underlying millisecond timestamp differs under New York timezone +SELECT toUnixTimestamp64Milli(toDate('2024-01-15') + toTime64('00:00:00.123', 3)) AS ts64_ny; + +-- Side-by-side with INTERVAL under New York timezone +SELECT (toDate('2024-01-15') + toTime(3723)) = (toDate('2024-01-15') + INTERVAL 3723 SECOND) AS same_ny; + +SET session_timezone = 'Asia/Kolkata'; +SELECT toDate('2024-01-15') + toTime('01:02:03') AS dtest_kol; +SELECT toUnixTimestamp(toDate('2024-01-15') + toTime(0)) AS ts_kol; + +-- Side-by-side with INTERVAL under Kolkata timezone +SELECT (toDate('2024-01-15') + toTime(3723)) = (toDate('2024-01-15') + INTERVAL 3723 SECOND) AS same_kol; + +-- Verify timestamps differ between timezones (proving tz affects computation) +SET session_timezone = 'UTC'; +SELECT + toUnixTimestamp(toDateTime('2024-01-15 00:00:00', 'UTC')) AS ts_utc, + toUnixTimestamp(toDateTime('2024-01-15 00:00:00', 'America/New_York')) AS ts_ny, + ts_utc != ts_ny AS differ; + +-- DST: Date+Time adds raw seconds to midnight, which can differ from parsing a +-- local timestamp string when DST transitions create gaps or overlaps. + +SET session_timezone = 'Europe/London'; + +-- 2023-10-29: London clocks fall back from 02:00 to 01:00 (01:30 occurs twice). +-- Date+Time gives midnight+5400s. toDateTime parses '01:30' picking the earlier occurrence. +-- Both resolve to the same instant, so the result is equal. +SELECT (toDate('2023-10-29') + toTime('01:30:00')) = toDateTime('2023-10-29 01:30:00', 'Europe/London'); + +-- 2023-03-26: London clocks spring forward from 01:00 to 02:00 (01:30 does not exist). +-- Date+Time gives midnight+5400s = 02:30 BST (the gap is skipped arithmetically). +-- toDateTime parses nonexistent '01:30' as 00:30 GMT (shifts back before the gap). +-- These are different instants, so the result is not equal. +SELECT (toDate('2023-03-26') + toTime('01:30:00')) = toDateTime('2023-03-26 01:30:00', 'Europe/London'); +SELECT (toDate('2023-03-26') + toTime64('01:30:00.123', 3)) = toDateTime64('2023-03-26 01:30:00.123', 3, 'Europe/London'); + +-- Show the actual values: 02:30 vs 00:30, different unix timestamps +SELECT + toDate('2023-03-26') + toTime('01:30:00') AS combined, + toDateTime('2023-03-26 01:30:00', 'Europe/London') AS parsed; +SELECT + toUnixTimestamp(toDate('2023-03-26') + toTime('01:30:00')) AS combined_ts, + toUnixTimestamp(toDateTime('2023-03-26 01:30:00', 'Europe/London')) AS parsed_ts; + +-- Date+Time matches midnight+INTERVAL (both just add raw seconds to midnight) +SELECT + (toDate('2023-03-26') + toTime('01:30:00')) = + (toDateTime('2023-03-26 00:00:00', 'Europe/London') + INTERVAL 5400 SECOND); + +SET session_timezone = 'UTC'; + +-- Common-type test: Date+Time and Date+Time64(0) should unify to DateTime64(0) +SELECT DISTINCT toTypeName(dt) FROM +( + SELECT toDate('2024-01-15') + toTime('01:02:03') AS dt + UNION ALL + SELECT toDate('2024-01-15') + toTime64('01:02:03', 0) AS dt +); + +-- Error cases + +SELECT toDate('2024-01-15') - toTime('01:02:03'); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toTime('01:02:03') - toDate('2024-01-15'); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toDateTime('2024-01-15 00:00:00') + toTime('01:02:03'); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toDate('2024-01-15') - toTime64('01:02:03.456', 3); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toDateTime('2024-01-15 00:00:00') + toTime64('01:02:03.456', 3); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toDateTime64('2024-01-15 00:00:00.000', 3) + toTime('01:02:03'); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } +SELECT toDateTime64('2024-01-15 00:00:00.000', 3) + toTime64('01:02:03.456', 3); -- { serverError ILLEGAL_TYPE_OF_ARGUMENT } + +-- Overflow with throw (already the default from top of file) + +-- Date + Time -> DateTime: underflow below epoch +SELECT toDate('1970-01-01') + toTime(-1); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- Date + Time -> DateTime: max valid DateTime +SELECT toDate('2106-02-07') + toTime('06:28:15'); +-- Date + Time -> DateTime: one second past max +SELECT toDate('2106-02-07') + toTime('06:28:16'); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- Date + Time -> DateTime: Date max far exceeds DateTime range +SELECT toDate('2149-06-06') + toTime('00:00:00'); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } + +-- Date32 + Time -> DateTime64(0): underflow below 1900 +SELECT toDate32('1900-01-01') + toTime(-1); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- Date32 + Time64 -> DateTime64: underflow below 1900 +SELECT toDate32('1900-01-01') + toTime64('-00:00:00.000001', 6); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } + +-- DateTime64(9) specific boundary: lower limit (1900-01-01 midnight) +SELECT toDate32('1900-01-01') + toTime64('00:00:00.000000000', 9) AS dt, toTypeName(dt); +-- DateTime64(9): a value on the last valid day (exact upper limit is 23:47:16.854775807 below) +SELECT toDate32('2262-04-11') + toTime64('23:47:16.000000000', 9) AS dt, toTypeName(dt); +-- DateTime64(9) specific boundary: past Int64 capacity at scale 9 +SELECT toDate32('2262-04-12') + toTime64('00:00:00.000000000', 9); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } + +-- One tick past the last representable DateTime64(9) value +SELECT toDate32('2262-04-11') + toTime64('23:47:16.854775808', 9); -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +-- Last representable tick at scale 9 +SELECT toDate32('2262-04-11') + toTime64('23:47:16.854775807', 9) AS dt, toTypeName(dt); + +-- Vector path: one row valid, one row past DateTime64(9) limit +DROP TABLE IF EXISTS test_overflow_int64; +CREATE TABLE test_overflow_int64 (d Date32, t Time64(9)) ENGINE = Memory; +INSERT INTO test_overflow_int64 VALUES + ('2262-04-11', '23:47:16.854775807'), + ('2262-04-11', '23:47:16.854775808'); +SELECT d + t FROM test_overflow_int64 ORDER BY t; -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +DROP TABLE test_overflow_int64; + +-- Vector overflow regression: throw must work on non-const columns too +DROP TABLE IF EXISTS test_overflow_vec; +CREATE TABLE test_overflow_vec (d Date, t Time) ENGINE = Memory; +INSERT INTO test_overflow_vec VALUES + ('2106-02-07', '06:28:15'), + ('2149-06-06', '00:00:00'); +SELECT d + t FROM test_overflow_vec ORDER BY d; -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +DROP TABLE test_overflow_vec; + +-- Vector path: Date32 + Time64(9) with one row past the DateTime64(9) limit +DROP TABLE IF EXISTS test_overflow_vec64; +CREATE TABLE test_overflow_vec64 (d Date32, t Time64(9)) ENGINE = Memory; +INSERT INTO test_overflow_vec64 VALUES + ('2262-04-11', '23:47:16.000000000'), + ('2262-04-12', '00:00:00.000000000'); +SELECT d + t FROM test_overflow_vec64 ORDER BY d; -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +DROP TABLE test_overflow_vec64; + +-- Large negative Time64 can bring an intermediate overflow back into range. +-- midnight(2299-12-31) * 10^9 overflows Int64, but subtracting 1.2B seconds lands in range. +SELECT toDate32('2299-12-31') + CAST(toDecimal128('-1200000000.000000000', 9), 'Time64(9)'); + +-- Vector path: one row has large negative time (in range), other row overflows. +DROP TABLE IF EXISTS test_intermediate_overflow; +CREATE TABLE test_intermediate_overflow (t Time64(9)) ENGINE = Memory; +INSERT INTO test_intermediate_overflow SELECT arrayJoin([CAST(toDecimal128('-1200000000.000000000', 9), 'Time64(9)'), toTime64('00:00:00.000000000', 9)]); +SELECT toDate32('2299-12-31') + t FROM test_intermediate_overflow ORDER BY t; -- { serverError VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE } +DROP TABLE test_intermediate_overflow; + +-- Overflow with saturate + +SET date_time_overflow_behavior = 'saturate'; + +-- Date + Time: underflow saturates to epoch +SELECT toDate('1970-01-01') + toTime(-1); +-- Date + Time: overflow saturates to DateTime max +SELECT toDate('2149-06-06') + toTime('23:59:59'); +-- Date32 + Time64: underflow saturates to DateTime64 min +SELECT toDate32('1900-01-01') + toTime64('-00:00:00.000001', 6); +-- DateTime64(9) overflow saturates to the last representable value +SELECT toDate32('2262-04-11') + toTime64('23:47:16.854775808', 9); + +-- Large negative time brings intermediate overflow back into range (should NOT saturate) +SELECT toDate32('2299-12-31') + CAST(toDecimal128('-1200000000.000000000', 9), 'Time64(9)'); + +-- Vector: first row is in range despite intermediate overflow, second row saturates +DROP TABLE IF EXISTS test_saturate_intermediate; +CREATE TABLE test_saturate_intermediate (t Time64(9)) ENGINE = Memory; +INSERT INTO test_saturate_intermediate SELECT arrayJoin([CAST(toDecimal128('-1200000000.000000000', 9), 'Time64(9)'), toTime64('00:00:00.000000000', 9)]); +SELECT toDate32('2299-12-31') + t FROM test_saturate_intermediate ORDER BY t; +DROP TABLE test_saturate_intermediate; + +SET date_time_overflow_behavior = 'throw'; + +-- Time values beyond the visible range display as saturated (999:59:59 or -999:59:59 +-- depending on sign) but internally store their full numeric value. Date+Time uses +-- the internal value, so two Time values that print identically can produce different +-- DateTime results. +SELECT + toTime(9999999) AS t_raw, + toTime(3599999) AS t_vis, + t_raw = t_vis AS same_time; +SELECT + toDate('2024-01-15') + toTime(9999999) AS dt_raw, + toDate('2024-01-15') + toTime(3599999) AS dt_vis, + dt_raw = dt_vis AS same_dt; +SELECT + (toDate('2024-01-15') + toTime(9999999)) = + (toDate('2024-01-15') + toTime(3599999)) AS same_dt_from_same_visible_time;