@@ -1106,4 +1106,147 @@ TEST(testCase, function_taosTimeTruncate) {
11061106 ASSERT_LE (res, 1633450000000 );
11071107}
11081108
1109+ /*
1110+ * Test: day-interval alignment under DST timezone (America/New_York).
1111+ *
1112+ * America/New_York has two UTC offsets:
1113+ * Winter (EST): UTC-5 (-18000s)
1114+ * Summer (EDT): UTC-4 (-14400s)
1115+ *
1116+ * For INTERVAL(1d) TIMEZONE('America/New_York'):
1117+ * - A winter timestamp should align to midnight EST = 05:00 UTC
1118+ * - A summer timestamp should align to midnight EDT = 04:00 UTC
1119+ *
1120+ * Historical BUG being tested: taosTimeTruncate internally calls
1121+ * taosGetTZOffsetSeconds() which returns the offset for "now" (the
1122+ * current wall-clock time), not the offset for the target timestamp.
1123+ * So if "now" is summer, a winter timestamp gets the EDT offset,
1124+ * making the day boundary 04:00 UTC instead of the correct
1125+ * 05:00 UTC — off by 1 hour.
1126+ */
1127+ #ifndef WINDOWS
1128+ // RAII guard to restore global timezone even if test assertions fail mid-way.
1129+ struct TzRestoreGuard {
1130+ ~TzRestoreGuard () { taosSetGlobalTimezone (" Asia/Shanghai" ); }
1131+ };
1132+
1133+ TEST (testCase, taosTimeTruncate_DST_day_interval) {
1134+ TzRestoreGuard tzGuard; // restores Asia/Shanghai on scope exit
1135+
1136+ // Setup: create a timezone object for America/New_York
1137+ timezone_t ny = tzalloc (" America/New_York" );
1138+ ASSERT_NE (ny, nullptr );
1139+
1140+ // Set global timezone too (for consistency)
1141+ ASSERT_EQ (taosSetGlobalTimezone (" America/New_York" ), TSDB_CODE_SUCCESS);
1142+
1143+ // -- Prepare timestamps (millisecond precision, TSDB_TIME_PRECISION_MILLI=0) --
1144+ //
1145+ // Winter: 2024-01-15 15:30:00 UTC = 2024-01-15 10:30:00 EST
1146+ // epoch_ms = 1705329000000
1147+ // Correct day start = 2024-01-15 00:00:00 EST = 2024-01-15 05:00:00 UTC
1148+ // = 1705294800000 ms
1149+ //
1150+ // Summer: 2024-07-15 15:30:00 UTC = 2024-07-15 11:30:00 EDT
1151+ // epoch_ms = 1721057400000
1152+ // Correct day start = 2024-07-15 00:00:00 EDT = 2024-07-15 04:00:00 UTC
1153+ // = 1721016000000 ms
1154+
1155+ const int64_t winter_ts_ms = 1705329000000LL ; // 2024-01-15 15:30:00 UTC
1156+ const int64_t winter_day_start_ms = 1705294800000LL ; // 2024-01-15 05:00:00 UTC (midnight EST)
1157+
1158+ const int64_t summer_ts_ms = 1721057400000LL ; // 2024-07-15 15:30:00 UTC
1159+ const int64_t summer_day_start_ms = 1721016000000LL ; // 2024-07-15 04:00:00 UTC (midnight EDT)
1160+
1161+ // Verify expected timestamps with taosLocalTime
1162+ {
1163+ time_t t;
1164+ struct tm tm_val;
1165+
1166+ // Verify winter day start is indeed midnight EST
1167+ t = (time_t )(winter_day_start_ms / 1000 );
1168+ ASSERT_NE (taosLocalTime (&t, &tm_val, NULL , 0 , ny), nullptr );
1169+ ASSERT_EQ (tm_val.tm_hour , 0 ) << " winter day start should be midnight local" ;
1170+ ASSERT_EQ (tm_val.tm_min , 0 );
1171+ ASSERT_EQ (tm_val.tm_sec , 0 );
1172+ ASSERT_EQ (tm_val.tm_mon + 1 , 1 ); // January
1173+ ASSERT_EQ (tm_val.tm_mday , 15 );
1174+
1175+ // Verify summer day start is indeed midnight EDT
1176+ t = (time_t )(summer_day_start_ms / 1000 );
1177+ ASSERT_NE (taosLocalTime (&t, &tm_val, NULL , 0 , ny), nullptr );
1178+ ASSERT_EQ (tm_val.tm_hour , 0 ) << " summer day start should be midnight local" ;
1179+ ASSERT_EQ (tm_val.tm_min , 0 );
1180+ ASSERT_EQ (tm_val.tm_sec , 0 );
1181+ ASSERT_EQ (tm_val.tm_mon + 1 , 7 ); // July
1182+ ASSERT_EQ (tm_val.tm_mday , 15 );
1183+ }
1184+
1185+ // Build SInterval for INTERVAL(1d)
1186+ const int64_t one_day_ms = 86400LL * 1000 ;
1187+ SInterval interval = {};
1188+ interval.timezone = ny;
1189+ interval.intervalUnit = ' d' ;
1190+ interval.slidingUnit = ' d' ;
1191+ interval.offsetUnit = 0 ;
1192+ interval.precision = TSDB_TIME_PRECISION_MILLI;
1193+ interval.interval = one_day_ms;
1194+ interval.sliding = one_day_ms;
1195+ interval.offset = 0 ;
1196+ interval.timeRange .skey = INT64_MIN;
1197+ interval.timeRange .ekey = INT64_MAX;
1198+
1199+ // -- Test winter timestamp --
1200+ int64_t winter_result = taosTimeTruncate (winter_ts_ms, &interval);
1201+
1202+ // Convert result to local time to show what we got
1203+ {
1204+ time_t t = (time_t )(winter_result / 1000 );
1205+ struct tm tm_val;
1206+ taosLocalTime (&t, &tm_val, NULL , 0 , ny);
1207+ std::cout << " Winter ts truncated to: "
1208+ << (1900 + tm_val.tm_year ) << " -"
1209+ << (tm_val.tm_mon + 1 ) << " -" << tm_val.tm_mday
1210+ << " " << tm_val.tm_hour << " :" << tm_val.tm_min << " :" << tm_val.tm_sec
1211+ << " (epoch_ms=" << winter_result << " )" << std::endl;
1212+ std::cout << " Expected: epoch_ms=" << winter_day_start_ms << std::endl;
1213+ }
1214+
1215+ EXPECT_EQ (winter_result, winter_day_start_ms)
1216+ << " Winter day boundary should be midnight EST (05:00 UTC). "
1217+ " If this fails, taosTimeTruncate used the current DST offset "
1218+ " instead of the winter offset." ;
1219+
1220+ // -- Test summer timestamp --
1221+ int64_t summer_result = taosTimeTruncate (summer_ts_ms, &interval);
1222+
1223+ {
1224+ time_t t = (time_t )(summer_result / 1000 );
1225+ struct tm tm_val;
1226+ taosLocalTime (&t, &tm_val, NULL , 0 , ny);
1227+ std::cout << " Summer ts truncated to: "
1228+ << (1900 + tm_val.tm_year ) << " -"
1229+ << (tm_val.tm_mon + 1 ) << " -" << tm_val.tm_mday
1230+ << " " << tm_val.tm_hour << " :" << tm_val.tm_min << " :" << tm_val.tm_sec
1231+ << " (epoch_ms=" << summer_result << " )" << std::endl;
1232+ std::cout << " Expected: epoch_ms=" << summer_day_start_ms << std::endl;
1233+ }
1234+
1235+ EXPECT_EQ (summer_result, summer_day_start_ms)
1236+ << " Summer day boundary should be midnight EDT (04:00 UTC). "
1237+ " If this fails, taosTimeTruncate used the current DST offset "
1238+ " instead of the summer offset." ;
1239+
1240+ // After the fix, BOTH seasons should align correctly.
1241+ bool both_correct = (winter_result == winter_day_start_ms) &&
1242+ (summer_result == summer_day_start_ms);
1243+ ASSERT_TRUE (both_correct)
1244+ << " Both winter and summer day boundaries should be correct "
1245+ " now that taosTimeTruncate uses per-timestamp DST resolution." ;
1246+
1247+ tzfree (ny);
1248+ // TzRestoreGuard destructor handles taosSetGlobalTimezone("Asia/Shanghai")
1249+ }
1250+ #endif
1251+
11091252#pragma GCC diagnostic pop
0 commit comments