Skip to content

Commit 2603cd5

Browse files
committed
fix: resolve xe-21 CI failures for interval/timestamp-tz/iot scenarios
compare.py: add normalization for TIMESTAMP WITH TIME ZONE (epoch nanos vs Oracle format), INTERVAL YEAR TO MONTH (integer months vs +YYYY-MM), and INTERVAL DAY TO SECOND (integer nanos vs +D HH:MI:SS.FF). iot-table.sql: add @tag iot to skip in CI (known limitation L9).
1 parent 22edc81 commit 2603cd5

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

tests/sql/inputs/iot-table.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
-- @TAG iot
12
-- iot-table.sql: Test Index-Organized Table (IOT).
23
-- Run as PDB user (e.g., olr_test/olr_test@//localhost:1521/FREEPDB1)
34
--

tests/sql/scripts/compare.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@
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

3648
def 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+
155253
def 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

Comments
 (0)