Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions programs/client/Client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include <Common/Config/ConfigProcessor.h>
#include <Common/Config/getClientConfigPath.h>
#include <Common/CurrentThread.h>
#include <Common/DateLUT.h>
#include <Common/DateLUTImpl.h>
#include <Common/Exception.h>
#include <Common/TerminalSize.h>
#include <Common/config_version.h>
Expand Down Expand Up @@ -449,6 +451,13 @@ void Client::connect()
UInt64 server_version_minor = 0;
UInt64 server_version_patch = 0;

/// Capture the client local time zone before the branch below may switch the process default
/// to the server time zone. `serverTimezoneInstance()` reads the process default directly and
/// ignores `session_timezone`; `instance()` would fold in an explicit `--session_timezone` and
/// cache the wrong zone. `connect()` can run again on reconnect, so only capture once.
if (client_local_timezone.empty())
client_local_timezone = DateLUT::serverTimezoneInstance().getTimeZone();

if (hosts_and_ports.empty())
{
String host = config().getString("host", "localhost");
Expand Down
12 changes: 12 additions & 0 deletions src/Client/ClientBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ namespace Setting
extern const SettingsString promql_database;
extern const SettingsString promql_table;
extern const SettingsFloatAuto evaluation_time;
extern const SettingsBool use_client_time_zone;
extern const SettingsTimezone session_timezone;
}

namespace ErrorCodes
Expand Down Expand Up @@ -2257,6 +2259,16 @@ void ClientBase::processParsedSingleQuery(

applySettingsFromServerIfNeeded(); // after connect() and applySettingsFromQuery()

/// With `use_client_time_zone`, DateTime string literals must be interpreted in the client time
/// zone. The client parses synchronous INSERT literals itself, but literals interpreted server-side
/// (asynchronous INSERT, SELECT) rely on `session_timezone`. Seed it with the client time zone unless
/// the user set `session_timezone` explicitly. This is transient (reverted with the other query
/// settings below), so it tracks per-query `use_client_time_zone` changes in both directions.
if (!client_local_timezone.empty()
&& client_context->getSettingsRef()[Setting::use_client_time_zone]
&& !client_context->getSettingsRef().isChanged("session_timezone"))
client_context->setSetting("session_timezone", client_local_timezone);

ASTPtr input_function;
const auto * insert = parsed_query->as<ASTInsertQuery>();
if (insert && insert->select)
Expand Down
5 changes: 5 additions & 0 deletions src/Client/ClientBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ class ClientBase
ContextMutablePtr global_context;
ContextMutablePtr client_context;

/// The client local time zone, captured on the first connect() before it may switch the
/// process default to the server time zone. Used to seed `session_timezone` per query when
/// `use_client_time_zone` is set, so server-side literal parsing matches the client side.
String client_local_timezone;

String default_database;
String query_id;
Int32 suggestion_limit;
Expand Down
12 changes: 12 additions & 0 deletions src/DataTypes/DataTypeDateTime.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <DataTypes/DataTypeDateTime.h>
#include <DataTypes/Serializations/SerializationDateTime.h>

#include <Common/DateLUT.h>
#include <Common/DateLUTImpl.h>

#include <IO/Operators.h>
Expand Down Expand Up @@ -38,6 +39,17 @@ bool DataTypeDateTime::equals(const IDataType & rhs) const

SerializationPtr DataTypeDateTime::doGetDefaultSerialization() const
{
if (!has_explicit_time_zone)
{
/// When no explicit timezone, resolve the effective timezone (respects session_timezone).
/// This is called once per formatter (not per row), so the cost of DateLUT::instance() is negligible.
const auto & effective_tz = DateLUT::instance();
if (&effective_tz != &time_zone)
{
TimezoneMixin overridden(effective_tz.getTimeZone());
return std::make_shared<SerializationDateTime>(overridden);
}
}
return std::make_shared<SerializationDateTime>(*this);
}

Expand Down
11 changes: 11 additions & 0 deletions src/DataTypes/DataTypeDateTime64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ bool DataTypeDateTime64::equals(const IDataType & rhs) const

SerializationPtr DataTypeDateTime64::doGetDefaultSerialization() const
{
if (!has_explicit_time_zone)
{
/// When no explicit timezone, resolve the effective timezone (respects session_timezone).
/// This is called once per formatter (not per row), so the cost of DateLUT::instance() is negligible.
const auto & effective_tz = DateLUT::instance();
if (&effective_tz != &time_zone)
{
TimezoneMixin overridden(effective_tz.getTimeZone());
return std::make_shared<SerializationDateTime64>(scale, overridden);
}
}
return std::make_shared<SerializationDateTime64>(scale, *this);
}

Expand Down
22 changes: 22 additions & 0 deletions src/Interpreters/InterpreterAlterQuery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <Interpreters/DatabaseCatalog.h>
#include <Interpreters/FunctionNameNormalizer.h>
#include <Interpreters/InterpreterCreateQuery.h>
#include <Interpreters/MutationsDateTimeLiteralVisitor.h>
#include <Interpreters/MutationsInterpreter.h>
#include <Interpreters/MutationsNonDeterministicHelpers.h>
#include <Interpreters/QueryLog.h>
Expand Down Expand Up @@ -44,6 +45,7 @@ namespace Setting
extern const SettingsSeconds lock_acquire_timeout;
extern const SettingsAlterUpdateMode alter_update_mode;
extern const SettingsBool enable_lightweight_update;
extern const SettingsTimezone session_timezone;
}

namespace ServerSetting
Expand Down Expand Up @@ -188,6 +190,26 @@ BlockIO InterpreterAlterQuery::executeToTable(const ASTAlterQuery & alter)
}
}

/// When session_timezone is set, string literals compared to DateTime columns
/// must be wrapped with explicit timezone to avoid misinterpretation in the
/// background mutation thread which lacks the session context.
const auto & session_tz = settings[Setting::session_timezone].value;
if (!session_tz.empty())
{
const auto & source_ast = *mut_command->ast->as<ASTAlterCommand>();
auto tz_rewritten_ast = rewriteDateTimeLiteralsWithTimezone(
source_ast, table->getInMemoryMetadata().columns, session_tz);
if (tz_rewritten_ast)
{
auto * tz_alter_command = tz_rewritten_ast->as<ASTAlterCommand>();
mut_command = MutationCommand::parse(tz_alter_command);
if (!mut_command)
throw Exception(ErrorCodes::LOGICAL_ERROR,
"Alter command '{}' is rewritten to invalid command '{}'",
source_ast.formatForErrorMessage(), tz_rewritten_ast->formatForErrorMessage());
}
}

mutation_commands.emplace_back(std::move(*mut_command));
}
else
Expand Down
229 changes: 229 additions & 0 deletions src/Interpreters/MutationsDateTimeLiteralVisitor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
#include <Interpreters/MutationsDateTimeLiteralVisitor.h>
#include <Interpreters/InDepthNodeVisitor.h>
#include <Parsers/ASTAlterQuery.h>
#include <Parsers/ASTFunction.h>
#include <Parsers/ASTIdentifier.h>
#include <Parsers/ASTLiteral.h>
#include <Parsers/ASTSelectQuery.h>
#include <DataTypes/DataTypeLowCardinality.h>
#include <DataTypes/DataTypeNullable.h>
#include <DataTypes/DataTypeDateTime.h>
#include <DataTypes/DataTypeDateTime64.h>

namespace DB
{

namespace
{

/// Returns the DateTime/DateTime64 column type if `identifier_name` refers to
/// a DateTime column without an explicit timezone. Returns nullptr otherwise.
DataTypePtr getDateTimeColumnType(const String & identifier_name, const ColumnsDescription & columns)
{
const auto * desc = columns.tryGet(identifier_name);
if (!desc)
return nullptr;

auto unwrapped = removeNullable(removeLowCardinality(desc->type));

if (const auto * dt = typeid_cast<const DataTypeDateTime *>(unwrapped.get()))
{
if (!dt->hasExplicitTimeZone())
return unwrapped;
}
else if (const auto * dt64 = typeid_cast<const DataTypeDateTime64 *>(unwrapped.get()))
{
if (!dt64->hasExplicitTimeZone())
return unwrapped;
}
return nullptr;
}

/// Wraps a string literal AST with toDateTime('...', 'tz') or toDateTime64('...', scale, 'tz').
ASTPtr wrapWithTimezone(const ASTPtr & literal_ast, const DataTypePtr & datetime_type, const String & timezone)
{
auto tz_literal = std::make_shared<ASTLiteral>(Field(timezone));

if (const auto * dt64 = typeid_cast<const DataTypeDateTime64 *>(datetime_type.get()))
{
auto scale_literal = std::make_shared<ASTLiteral>(Field(static_cast<UInt64>(dt64->getScale())));
return makeASTFunction("toDateTime64", literal_ast, std::move(scale_literal), std::move(tz_literal));
}

return makeASTFunction("toDateTime", literal_ast, std::move(tz_literal));
}

/// For a comparison like `column >= 'datetime-string'`, try to wrap the string
/// literal with an explicit timezone cast. Modifies the AST in place.
/// Returns true if any literal was wrapped.
bool tryWrapComparisonLiteral(ASTFunction & function, const ColumnsDescription & columns, const String & timezone)
{
if (!function.arguments || function.arguments->children.size() != 2)
return false;

bool wrapped = false;
auto & left = function.arguments->children[0];
auto & right = function.arguments->children[1];

/// Check left=identifier, right=string-literal
if (const auto * id = left->as<ASTIdentifier>())
{
if (auto dt = getDateTimeColumnType(id->name(), columns))
{
if (const auto * lit = right->as<ASTLiteral>(); lit && lit->value.getType() == Field::Types::String)
{
right = wrapWithTimezone(right, dt, timezone);
wrapped = true;
}
}
}

/// Check right=identifier, left=string-literal (e.g. '2000-01-01' <= time)
if (const auto * id = right->as<ASTIdentifier>())
{
if (auto dt = getDateTimeColumnType(id->name(), columns))
{
if (const auto * lit = left->as<ASTLiteral>(); lit && lit->value.getType() == Field::Types::String)
{
left = wrapWithTimezone(left, dt, timezone);
wrapped = true;
}
}
}

return wrapped;
}

/// For an IN expression like `column IN ('dt1', 'dt2')`, wrap each string literal.
/// Returns true if any literal was wrapped.
bool tryWrapInLiterals(ASTFunction & function, const ColumnsDescription & columns, const String & timezone)
{
if (!function.arguments || function.arguments->children.size() != 2)
return false;

const auto & left = function.arguments->children[0];
auto & right = function.arguments->children[1];

const auto * id = left->as<ASTIdentifier>();
if (!id)
return false;

auto dt = getDateTimeColumnType(id->name(), columns);
if (!dt)
return false;

bool wrapped = false;

/// The right side of IN can be an ASTFunction (tuple) or ASTExpressionList.
/// Walk its children and wrap string literals.
auto wrap_children = [&](ASTs & children)
{
for (auto & child : children)
{
if (const auto * lit = child->as<ASTLiteral>(); lit && lit->value.getType() == Field::Types::String)
{
child = wrapWithTimezone(child, dt, timezone);
wrapped = true;
}
}
};

if (auto * tuple_func = right->as<ASTFunction>(); tuple_func && tuple_func->name == "tuple" && tuple_func->arguments)
wrap_children(tuple_func->arguments->children);
else if (auto * expr_list = right->as<ASTExpressionList>())
wrap_children(expr_list->children);

return wrapped;
}

const std::unordered_set<String> comparison_functions = {
"equals", "notEquals", "less", "greater", "lessOrEquals", "greaterOrEquals",
};

const std::unordered_set<String> in_functions = {
"in", "notIn", "globalIn", "globalNotIn",
};

class RewriteDateTimeLiteralsMatcher
{
public:
struct Data
{
const ColumnsDescription & columns;
const String & session_timezone;
bool modified = false;
};

static bool needChildVisit(const ASTPtr & ast, const ASTPtr & /*child*/)
{
return !ast->as<ASTSelectQuery>();
}

static void visit(ASTPtr & ast, Data & data)
{
if (auto * function = ast->as<ASTFunction>())
visit(*function, data);
}

static void visit(ASTFunction & function, Data & data)
{
if (comparison_functions.contains(function.name))
{
if (tryWrapComparisonLiteral(function, data.columns, data.session_timezone))
data.modified = true;
}
else if (in_functions.contains(function.name))
{
if (tryWrapInLiterals(function, data.columns, data.session_timezone))
data.modified = true;
}
}
};

using RewriteDateTimeLiteralsVisitor = InDepthNodeVisitor<RewriteDateTimeLiteralsMatcher, true>;

}

ASTPtr rewriteDateTimeLiteralsWithTimezone(
const ASTAlterCommand & alter_command,
const ColumnsDescription & columns,
const String & session_timezone)
{
if (session_timezone.empty())
return nullptr;

auto query = alter_command.clone();
auto & new_command = *query->as<ASTAlterCommand>();

auto remove_child = [](auto & children, IAST *& erase_ptr)
{
auto it = std::find_if(children.begin(), children.end(), [&](const auto & ptr) { return ptr.get() == erase_ptr; });
erase_ptr = nullptr;
children.erase(it);
};

RewriteDateTimeLiteralsMatcher::Data data{columns, session_timezone, false};
RewriteDateTimeLiteralsVisitor visitor(data);

if (new_command.update_assignments)
{
ASTPtr update_assignments = new_command.update_assignments->clone();
remove_child(new_command.children, new_command.update_assignments);
visitor.visit(update_assignments);
new_command.update_assignments = new_command.children.emplace_back(std::move(update_assignments)).get();
}
if (new_command.predicate)
{
ASTPtr predicate = new_command.predicate->clone();
remove_child(new_command.children, new_command.predicate);
visitor.visit(predicate);
new_command.predicate = new_command.children.emplace_back(std::move(predicate)).get();
}

if (!data.modified)
return nullptr;

return query;
}

}
Loading
Loading