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