From ad6fcda6dfed8bf2475988bfdcbd5f9320a2a83d Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Mon, 13 May 2024 16:54:38 +0200 Subject: [PATCH 1/2] Ido*sqlConnection#FieldToEscapedString(): don't overflow timestamps > long --- lib/db_ido_mysql/idomysqlconnection.cpp | 4 ++-- lib/db_ido_pgsql/idopgsqlconnection.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/db_ido_mysql/idomysqlconnection.cpp b/lib/db_ido_mysql/idomysqlconnection.cpp index aacb7d7bd..74cb1d712 100644 --- a/lib/db_ido_mysql/idomysqlconnection.cpp +++ b/lib/db_ido_mysql/idomysqlconnection.cpp @@ -891,9 +891,9 @@ bool IdoMysqlConnection::FieldToEscapedString(const String& key, const Value& va *result = static_cast(dbrefcol); } else if (DbValue::IsTimestamp(value)) { - long ts = rawvalue; + double ts = rawvalue; std::ostringstream msgbuf; - msgbuf << "FROM_UNIXTIME(" << ts << ")"; + msgbuf << "FROM_UNIXTIME(" << std::fixed << std::setprecision(0) << ts << ")"; *result = Value(msgbuf.str()); } else if (DbValue::IsObjectInsertID(value)) { auto id = static_cast(rawvalue); diff --git a/lib/db_ido_pgsql/idopgsqlconnection.cpp b/lib/db_ido_pgsql/idopgsqlconnection.cpp index 07e88e6bb..db5629b82 100644 --- a/lib/db_ido_pgsql/idopgsqlconnection.cpp +++ b/lib/db_ido_pgsql/idopgsqlconnection.cpp @@ -700,9 +700,9 @@ bool IdoPgsqlConnection::FieldToEscapedString(const String& key, const Value& va *result = static_cast(dbrefcol); } else if (DbValue::IsTimestamp(value)) { - long ts = rawvalue; + double ts = rawvalue; std::ostringstream msgbuf; - msgbuf << "TO_TIMESTAMP(" << ts << ") AT TIME ZONE 'UTC'"; + msgbuf << "TO_TIMESTAMP(" << std::fixed << std::setprecision(0) << ts << ") AT TIME ZONE 'UTC'"; *result = Value(msgbuf.str()); } else if (DbValue::IsObjectInsertID(value)) { auto id = static_cast(rawvalue); From c6f9de5933bb0866bda3b942bd10c4078b1569e7 Mon Sep 17 00:00:00 2001 From: "Alexander A. Klimov" Date: Wed, 5 Jun 2024 15:14:00 +0200 Subject: [PATCH 2/2] Ido*sqlConnection#FieldToEscapedString(): don't write out of range time MySQL's FROM_UNIXTIME() NULLs ts <1970, errors for >2038. Postgres' TO_TIMESTAMP() errors for all ts not between 4713BC - 294276AD. --- lib/db_ido_mysql/idomysqlconnection.cpp | 6 +++- lib/db_ido_pgsql/idopgsqlconnection.cpp | 43 ++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/db_ido_mysql/idomysqlconnection.cpp b/lib/db_ido_mysql/idomysqlconnection.cpp index 74cb1d712..7af82e5fa 100644 --- a/lib/db_ido_mysql/idomysqlconnection.cpp +++ b/lib/db_ido_mysql/idomysqlconnection.cpp @@ -891,9 +891,13 @@ bool IdoMysqlConnection::FieldToEscapedString(const String& key, const Value& va *result = static_cast(dbrefcol); } else if (DbValue::IsTimestamp(value)) { + // MySQL TIMESTAMP columns have the year-2038 problem, hence the upper limit below. + // Also, they don't accept FROM_UNIXTIME(0): ERROR 1292 (22007): Incorrect datetime value: '1970-01-01 00:00:00' + double ts = rawvalue; std::ostringstream msgbuf; - msgbuf << "FROM_UNIXTIME(" << std::fixed << std::setprecision(0) << ts << ")"; + msgbuf << "FROM_UNIXTIME(" << std::fixed << std::setprecision(0) + << std::fmin(std::fmax(ts, 1.0), 2147483647.0) << ")"; *result = Value(msgbuf.str()); } else if (DbValue::IsObjectInsertID(value)) { auto id = static_cast(rawvalue); diff --git a/lib/db_ido_pgsql/idopgsqlconnection.cpp b/lib/db_ido_pgsql/idopgsqlconnection.cpp index db5629b82..a9c622afd 100644 --- a/lib/db_ido_pgsql/idopgsqlconnection.cpp +++ b/lib/db_ido_pgsql/idopgsqlconnection.cpp @@ -15,6 +15,7 @@ #include "base/context.hpp" #include "base/statsfunction.hpp" #include "base/defer.hpp" +#include #include using namespace icinga; @@ -700,9 +701,49 @@ bool IdoPgsqlConnection::FieldToEscapedString(const String& key, const Value& va *result = static_cast(dbrefcol); } else if (DbValue::IsTimestamp(value)) { + // In addition to the limits of PostgreSQL itself (4713BC - 294276AD), + // years not fitting in YYYY may cause problems, see e.g. https://github.com/golang/go/issues/4556. + // RFC 3339: "All dates and times are assumed to be (...) somewhere between 0000AD and 9999AD." + // The below limits include safety buffers to make sure the timestamps are within 0-9999 AD in all time zones: + // + // postgres=# \x + // Expanded display is on. + // postgres=# SELECT TO_TIMESTAMP(-62135510400) AT TIME ZONE 'UTC' AS utc, + // postgres-# TO_TIMESTAMP(-62135510400) AT TIME ZONE 'Asia/Vladivostok' AS east, + // postgres-# TO_TIMESTAMP(-62135510400) AT TIME ZONE 'America/Juneau' AS west, + // postgres-# TO_TIMESTAMP(-62135510400) AT TIME ZONE 'America/Nuuk' AS north; + // -[ RECORD 1 ]-------------- + // utc | 0001-01-02 00:00:00 + // east | 0001-01-02 08:47:31 + // west | 0001-01-02 15:02:19 + // north | 0001-01-01 20:33:04 + // + // postgres=# SELECT TO_TIMESTAMP(-62135510400-86400) AT TIME ZONE 'UTC' AS utc, + // postgres-# TO_TIMESTAMP(-62135510400-86400) AT TIME ZONE 'Asia/Vladivostok' AS east, + // postgres-# TO_TIMESTAMP(-62135510400-86400) AT TIME ZONE 'America/Juneau' AS west, + // postgres-# TO_TIMESTAMP(-62135510400-86400) AT TIME ZONE 'America/Nuuk' AS north; + // -[ RECORD 1 ]----------------- + // utc | 0001-01-01 00:00:00 + // east | 0001-01-01 08:47:31 + // west | 0001-01-01 15:02:19 + // north | 0001-12-31 20:33:04 BC + // + // postgres=# SELECT TO_TIMESTAMP(253402214400) AT TIME ZONE 'UTC' AS utc, + // postgres-# TO_TIMESTAMP(253402214400) AT TIME ZONE 'Asia/Vladivostok' AS east, + // postgres-# TO_TIMESTAMP(253402214400) AT TIME ZONE 'America/Juneau' AS west, + // postgres-# TO_TIMESTAMP(253402214400) AT TIME ZONE 'America/Nuuk' AS north; + // -[ RECORD 1 ]------------- + // utc | 9999-12-31 00:00:00 + // east | 9999-12-31 10:00:00 + // west | 9999-12-30 15:00:00 + // north | 9999-12-30 22:00:00 + // + // postgres=# + double ts = rawvalue; std::ostringstream msgbuf; - msgbuf << "TO_TIMESTAMP(" << std::fixed << std::setprecision(0) << ts << ") AT TIME ZONE 'UTC'"; + msgbuf << "TO_TIMESTAMP(" << std::fixed << std::setprecision(0) + << std::fmin(std::fmax(ts, -62135510400.0), 253402214400.0) << ") AT TIME ZONE 'UTC'"; *result = Value(msgbuf.str()); } else if (DbValue::IsObjectInsertID(value)) { auto id = static_cast(rawvalue);