3232 '%d-%b-%y %H.%M.%S' , # DD-MON-RR HH.MI.SS (Oracle default with time)
3333]
3434
35+ # TIMESTAMP WITH TIME ZONE from LogMiner: DD-MON-RR HH.MI.SS.FF AM/PM +HH:MM
36+ ORACLE_TSTZ_RE = re .compile (
37+ r'^(\d{2})-([A-Z]{3})-(\d{2,4})\s+(\d{1,2})\.(\d{2})\.(\d{2})(?:\.(\d+))?\s*(AM|PM)\s+([+-]\d{2}:\d{2})$' ,
38+ re .IGNORECASE
39+ )
40+
41+ # INTERVAL YEAR TO MONTH from LogMiner: [+/-]YYYY-MM
42+ INTERVAL_YTM_RE = re .compile (r'^([+-])(\d+)-(\d+)$' )
43+
44+ # INTERVAL DAY TO SECOND from LogMiner: [+/-]D HH:MI:SS.FF
45+ INTERVAL_DTS_RE = re .compile (r'^([+-])(\d+)\s+(\d{2}):(\d{2}):(\d{2})\.(\d+)$' )
46+
3547
3648def normalize_value (v ):
3749 """Normalize a value to string for comparison. None stays None."""
@@ -152,6 +164,92 @@ def try_parse_oracle_datetime(s):
152164 return None , None
153165
154166
167+ def try_match_tstz (lm_val , olr_val ):
168+ """Try matching TIMESTAMP WITH TIME ZONE values.
169+
170+ LogMiner: '15-JUN-25 10.30.00.123456 AM +05:30'
171+ OLR: '1749963600123456000,+05:30' (epoch_nanos,tz_offset)
172+ """
173+ m_lm = ORACLE_TSTZ_RE .match (lm_val )
174+ if not m_lm :
175+ return None
176+ # Check OLR format: nanos,offset
177+ parts = olr_val .rsplit (',' , 1 )
178+ if len (parts ) != 2 or not parts [1 ].strip ().startswith (('+' , '-' )):
179+ return None
180+
181+ day , mon , year , hour , minute , sec , frac , ampm , lm_tz = m_lm .groups ()
182+ hour = int (hour )
183+ if ampm .upper () == 'PM' and hour != 12 :
184+ hour += 12
185+ elif ampm .upper () == 'AM' and hour == 12 :
186+ hour = 0
187+
188+ # Parse timezone offset to seconds
189+ tz_sign = 1 if lm_tz [0 ] == '+' else - 1
190+ tz_h , tz_m = lm_tz [1 :].split (':' )
191+ tz_offset_sec = tz_sign * (int (tz_h ) * 3600 + int (tz_m ) * 60 )
192+
193+ try :
194+ dt = datetime .strptime (f"{ day } -{ mon } -{ year } " , '%d-%b-%y' )
195+ dt = dt .replace (hour = hour , minute = int (minute ), second = int (sec ),
196+ tzinfo = timezone .utc )
197+ # Convert to UTC by subtracting timezone offset
198+ epoch_sec = int (dt .timestamp ()) - tz_offset_sec
199+ frac_nanos = int (frac .ljust (9 , '0' )[:9 ]) if frac else 0
200+ lm_nanos = epoch_sec * 1_000_000_000 + frac_nanos
201+ except ValueError :
202+ return None
203+
204+ olr_nanos_str , olr_tz = parts [0 ].strip (), parts [1 ].strip ()
205+ try :
206+ olr_nanos = int (olr_nanos_str )
207+ except ValueError :
208+ return None
209+
210+ return lm_nanos == olr_nanos and lm_tz .strip () == olr_tz
211+
212+
213+ def try_match_interval_ytm (lm_val , olr_val ):
214+ """Try matching INTERVAL YEAR TO MONTH values.
215+
216+ LogMiner: '+0005-03' (years-months)
217+ OLR: '63' (total months as integer)
218+ """
219+ m = INTERVAL_YTM_RE .match (lm_val )
220+ if not m :
221+ return None
222+ sign_str , years , months = m .groups ()
223+ sign = - 1 if sign_str == '-' else 1
224+ lm_months = sign * (int (years ) * 12 + int (months ))
225+ try :
226+ olr_months = int (olr_val )
227+ except ValueError :
228+ return None
229+ return lm_months == olr_months
230+
231+
232+ def try_match_interval_dts (lm_val , olr_val ):
233+ """Try matching INTERVAL DAY TO SECOND values.
234+
235+ LogMiner: '+0010 04:30:15.123456' (days hours:min:sec.frac)
236+ OLR: '880215123456000' (total nanoseconds as integer)
237+ """
238+ m = INTERVAL_DTS_RE .match (lm_val )
239+ if not m :
240+ return None
241+ sign_str , days , hours , minutes , seconds , frac = m .groups ()
242+ sign = - 1 if sign_str == '-' else 1
243+ total_sec = ((int (days ) * 24 + int (hours )) * 60 + int (minutes )) * 60 + int (seconds )
244+ frac_nanos = int (frac .ljust (9 , '0' )[:9 ])
245+ lm_nanos = sign * (total_sec * 1_000_000_000 + frac_nanos )
246+ try :
247+ olr_nanos = int (olr_val )
248+ except ValueError :
249+ return None
250+ return lm_nanos == olr_nanos
251+
252+
155253def values_match (lm_val , olr_val ):
156254 """Compare two normalized values with type awareness."""
157255 if lm_val is None and olr_val is None :
@@ -161,6 +259,18 @@ def values_match(lm_val, olr_val):
161259 # Direct string match
162260 if lm_val == olr_val :
163261 return True
262+ # Try TIMESTAMP WITH TIME ZONE
263+ result = try_match_tstz (lm_val , olr_val )
264+ if result is not None :
265+ return result
266+ # Try INTERVAL YEAR TO MONTH
267+ result = try_match_interval_ytm (lm_val , olr_val )
268+ if result is not None :
269+ return result
270+ # Try INTERVAL DAY TO SECOND
271+ result = try_match_interval_dts (lm_val , olr_val )
272+ if result is not None :
273+ return result
164274 # Try numeric comparison with tolerance for float precision differences
165275 # (e.g., BINARY_FLOAT: LogMiner='3.1400001E+000', OLR='3.14')
166276 try :
0 commit comments