From 1912d57bc68d732a635ce9d171c8ce11d147f74f Mon Sep 17 00:00:00 2001 From: kasemir Date: Fri, 12 Dec 2025 10:49:31 -0500 Subject: [PATCH] RDB archive: Fix Oracle time zone problem. The sample.smpl_time is handled differently for MySQL, Postgres, Oracle. In MySQL, it's stored as UTC. The `Timestamp` passed to `PreparedStatement.setTimestamp(Timestamp)` is converted to UTC and stored as UTC. On retrieval, `ResultSet.getTimestamp` converts back to the local time zone with appropriate GMT offset. In Postgres, we need to use a `TIMESTAMPTZ` datatype that stores the stamp with timezone info. Both MySQL and Postgres handle the fall transition from daylight savings time back to standard time just fine. In Oracle, even if we use `TIMESTAMP WITH TIMEZONE` for smpl_time, Oracle JDBC will second-guess the Timestamp passed to `setTimestamp(Timestamp)` and change the time offset. During the DST change in the fall, time stamps will be written with the wrong GMT offset. The workaround is to use `setObject(OffsetDateTime)` because OffsetDateTime is passed through and written as received. --- .../archive-engine/dbd/postgres_schema.txt | 2 +- .../archive/writer/rdb/RDBArchiveWriter.java | 13 +++--- .../archive/writer/rdb/TimestampHelper.java | 45 ++++++++++--------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/services/archive-engine/dbd/postgres_schema.txt b/services/archive-engine/dbd/postgres_schema.txt index 1649820a49..4947bd3b6c 100644 --- a/services/archive-engine/dbd/postgres_schema.txt +++ b/services/archive-engine/dbd/postgres_schema.txt @@ -199,7 +199,7 @@ DROP TABLE IF EXISTS sample; CREATE TABLE sample ( channel_id BIGINT NOT NULL, - smpl_time TIMESTAMP NOT NULL, + smpl_time TIMESTAMPTZ NOT NULL, nanosecs BIGINT NOT NULL, severity_id BIGINT NOT NULL, status_id BIGINT NOT NULL, diff --git a/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/RDBArchiveWriter.java b/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/RDBArchiveWriter.java index 38c54216ee..516a3f47fa 100644 --- a/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/RDBArchiveWriter.java +++ b/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/RDBArchiveWriter.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2011-2024 Oak Ridge National Laboratory. + * Copyright (c) 2011-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -51,7 +51,6 @@ * @author Lana Abadie - PostgreSQL for original RDBArchive code. Disable autocommit as needed. * @author Laurent Philippe (Use read-only connection when possible for MySQL load balancing) */ -@SuppressWarnings("nls") public class RDBArchiveWriter implements ArchiveWriter { /** Status string for Double.NaN samples */ @@ -430,9 +429,8 @@ private void oldBatchDoubleSamples(final RDBWriteChannel channel, final int N = additional.size(); for (int i = 1; i < N; i++) { - if (dialect == Dialect.Oracle){ - insert_array_sample.setTimestamp(2, stamp); - } + if (dialect == Dialect.Oracle) + insert_array_sample.setObject(2, TimestampHelper.toOffsetDateTime(stamp)); else { // Truncate the time stamp @@ -498,9 +496,8 @@ private void completeAndBatchInsert( final Timestamp stamp, final int severity, final Status status) throws Exception { - if (dialect == Dialect.Oracle){ - insert_xx.setTimestamp(2, stamp); - } + if (dialect == Dialect.Oracle) + insert_xx.setObject(2, TimestampHelper.toOffsetDateTime(stamp)); else { // Truncate the time stamp diff --git a/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/TimestampHelper.java b/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/TimestampHelper.java index 7c0b865ca9..909d74afbb 100644 --- a/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/TimestampHelper.java +++ b/services/archive-engine/src/main/java/org/csstudio/archive/writer/rdb/TimestampHelper.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2012-2018 Oak Ridge National Laboratory. + * Copyright (c) 2012-2025 Oak Ridge National Laboratory. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -9,9 +9,9 @@ import java.time.Duration; import java.time.Instant; +import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.Calendar; import java.util.concurrent.TimeUnit; import org.phoebus.util.time.TimestampFormats; @@ -19,7 +19,6 @@ /** Time stamp gymnastics * @author Kay Kasemir */ -@SuppressWarnings("nls") public class TimestampHelper { /** @param timestamp {@link Instant}, may be null @@ -32,17 +31,6 @@ public static String format(final Instant timestamp) return TimestampFormats.FULL_FORMAT.format(timestamp); } - // May look like just time_format.format(Instant) works, - // but results in runtime error " java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: YearOfEra" - // because time for printing needs to be in local time -// public static void main(String[] args) -// { -// final Instant now = Instant.now(); -// System.out.println(format(now)); -// System.out.println(time_format.format(now)); -// } - - /** @param timestamp EPICS Timestamp * @return SQL Timestamp */ @@ -77,14 +65,29 @@ public static Instant fromMillisecs(final long millisecs) } return Instant.ofEpochSecond(seconds, nanoseconds); } - - /** @param calendar Calendar - * @return EPICS Timestamp + + /** Zone ID is something like "America/New_York". + * Within that zone, time might change between + * EDT (daylight saving) and EST (standard), + * but the Zone ID remains, so we can keep it final. */ - public static Instant fromCalendar(final Calendar calendar) - { - return fromMillisecs(calendar.getTimeInMillis()); - } + final private static ZoneId zone = ZoneId.systemDefault(); + + /** Turn SQL {@link java.sql.Timestamp} into {@link OffsetDateTime} + * + * Oracle JDBC PreparedStatement.setTimestamp(int, Timestamp) + * will change the zone info in unexpected ways. + * Using PreparedStatement.setObject(int, OffsetDateTime) + * is the suggested workaround, so this morphs a Timestamp + * into OffsetDateTime + * + * @param sql_time SQL {@link java.sql.Timestamp} + * @return {@link OffsetDateTime} + */ + public static OffsetDateTime toOffsetDateTime(final java.sql.Timestamp sql_time) + { + return OffsetDateTime.ofInstant(sql_time.toInstant(), zone); + } /** Round time to next multiple of given duration * @param time Original time stamp