Skip to content

Commit 427047c

Browse files
Thin: return the same value for timestamp with time zone columns as thick
(resolves #7).
1 parent 1d0e563 commit 427047c

File tree

5 files changed

+409
-3
lines changed

5 files changed

+409
-3
lines changed

doc/src/release_notes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ oracledb 1.0.1 (TBD)
1414
#) Thick: restored support for bequeath connections.
1515
#) Thin: added support for multiple aliases in one entry in tnsnames.ora
1616
(`issue 3 <https://github.com/oracle/python-oracledb/issues/3>`__).
17+
#) Thin: return the same value for timestamp with time zone columns as thick
18+
(`issue 7 <https://github.com/oracle/python-oracledb/issues/7>`__).
1719
#) Ensured the name of wrapped functions are the same as the function being
1820
wrapped in order to improve error messages that reference them.
1921
#) Added exception class (oracledb.ConnectionError) as a subclass of

src/oracledb/impl/thin/buffer.pyx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -626,9 +626,11 @@ cdef class ReadBuffer:
626626
representing that value.
627627
"""
628628
cdef:
629-
const char_type *ptr
629+
int8_t tz_hour = 0, tz_minute = 0
630630
uint32_t fsecond = 0
631+
const char_type *ptr
631632
uint8_t num_bytes
633+
int32_t seconds
632634
uint16_t year
633635
self.read_ub1(&num_bytes)
634636
if _is_null_length(num_bytes):
@@ -637,8 +639,15 @@ cdef class ReadBuffer:
637639
year = (<uint8_t> ptr[0] - 100) * 100 + <uint8_t> ptr[1] - 100
638640
if num_bytes >= 11:
639641
fsecond = unpack_uint32(&ptr[7], BYTE_ORDER_MSB) // 1000
640-
return cydatetime.datetime_new(year, ptr[2], ptr[3], ptr[4] - 1,
641-
ptr[5] - 1, ptr[6] - 1, fsecond, None)
642+
value = cydatetime.datetime_new(year, ptr[2], ptr[3], ptr[4] - 1,
643+
ptr[5] - 1, ptr[6] - 1, fsecond, None)
644+
if num_bytes > 11 and ptr[11] != 0 and ptr[12] != 0:
645+
tz_hour = ptr[11] - TZ_HOUR_OFFSET
646+
tz_minute = ptr[12] - TZ_MINUTE_OFFSET
647+
if tz_hour != 0 or tz_minute != 0:
648+
seconds = tz_hour * 3600 + tz_minute * 60
649+
value += cydatetime.timedelta_new(0, seconds, 0)
650+
return value
642651

643652
cdef object read_interval_ds(self):
644653
"""

tests/sql/create_schema.sql

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,20 @@ create table &main_user..TestTimestamps (
237237
)
238238
/
239239

240+
create table &main_user..TestTimestampLTZs (
241+
IntCol number(9) not null,
242+
TimestampLTZCol timestamp with local time zone not null,
243+
NullableCol timestamp with local time zone
244+
)
245+
/
246+
247+
create table &main_user..TestTimestampTZs (
248+
IntCol number(9) not null,
249+
TimestampTZCol timestamp with time zone not null,
250+
NullableCol timestamp with time zone
251+
)
252+
/
253+
240254
create table &main_user..TestIntervals (
241255
IntCol number(9) not null,
242256
IntervalCol interval day to second not null,
@@ -416,6 +430,40 @@ begin
416430
end;
417431
/
418432

433+
begin
434+
for i in 1..10 loop
435+
insert into &main_user..TestTimestampLTZs
436+
values (i, to_timestamp_tz('20220602 ' ||
437+
decode(mod(i, 2), 0, '-', '+') ||
438+
ltrim(to_char(i, '00')) || ':' ||
439+
decode(mod(i, 4), 0, '00', '30'), 'YYYYMMDD TZH:TZM') +
440+
to_dsinterval(to_char(i) || ' 00:00:' || to_char(i * 2) ||
441+
'.' || to_char(i * 50)),
442+
decode(mod(i, 2), 0, to_timestamp(null, 'YYYYMMDD'),
443+
to_timestamp_tz('20220602 00:00', 'YYYYMMDD TZH:TZM') +
444+
to_dsinterval(to_char(i + 1) || ' 00:00:' ||
445+
to_char(i * 3) || '.' || to_char(i * 125))));
446+
end loop;
447+
end;
448+
/
449+
450+
begin
451+
for i in 1..10 loop
452+
insert into &main_user..TestTimestampTZs
453+
values (i, to_timestamp_tz('20220603 ' ||
454+
decode(mod(i, 2), 0, '-', '+') ||
455+
ltrim(to_char(i, '00')) || ':' ||
456+
decode(mod(i, 4), 0, '00', '30'), 'YYYYMMDD TZH:TZM') +
457+
to_dsinterval(to_char(i) || ' 00:00:' || to_char(i * 2) ||
458+
'.' || to_char(i * 50)),
459+
decode(mod(i, 2), 0, to_timestamp(null, 'YYYYMMDD'),
460+
to_timestamp_tz('20220603 00:00', 'YYYYMMDD TZH:TZM') +
461+
to_dsinterval(to_char(i + 1) || ' 00:00:' ||
462+
to_char(i * 3) || '.' || to_char(i * 125))));
463+
end loop;
464+
end;
465+
/
466+
419467
begin
420468
for i in 1..10 loop
421469
insert into &main_user..TestIntervals

tests/test_4800_timestamp_ltz_var.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#------------------------------------------------------------------------------
2+
# Copyright (c) 2022, Oracle and/or its affiliates.
3+
#
4+
# This software is dual-licensed to you under the Universal Permissive License
5+
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
6+
# 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
7+
# either license.
8+
#
9+
# If you elect to accept the software under the Apache License, Version 2.0,
10+
# the following applies:
11+
#
12+
# Licensed under the Apache License, Version 2.0 (the "License");
13+
# you may not use this file except in compliance with the License.
14+
# You may obtain a copy of the License at
15+
#
16+
# https://www.apache.org/licenses/LICENSE-2.0
17+
#
18+
# Unless required by applicable law or agreed to in writing, software
19+
# distributed under the License is distributed on an "AS IS" BASIS,
20+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21+
# See the License for the specific language governing permissions and
22+
# limitations under the License.
23+
#------------------------------------------------------------------------------
24+
25+
"""
26+
4800 - Module for testing timestamp with local time zone variables
27+
"""
28+
29+
import datetime
30+
31+
import oracledb
32+
import test_env
33+
34+
class TestCase(test_env.BaseTestCase):
35+
36+
def setUp(self):
37+
super().setUp()
38+
self.raw_data = []
39+
self.data_by_key = {}
40+
base_date = datetime.datetime(2022, 6, 2)
41+
self.cursor.execute("alter session set time_zone = 'UTC'")
42+
for i in range(1, 11):
43+
if i % 4 == 0:
44+
tz_hours = i
45+
elif i % 2 == 0:
46+
tz_hours = i + 0.5
47+
else:
48+
tz_hours = -(i + 0.5)
49+
tz_offset = datetime.timedelta(hours=tz_hours)
50+
microseconds = int(str(i * 50).ljust(6, "0"))
51+
offset = datetime.timedelta(days=i, seconds=i * 2,
52+
microseconds=microseconds)
53+
col = base_date + tz_offset + offset
54+
if i % 2:
55+
tz_offset = datetime.timedelta(hours=6)
56+
microseconds = int(str(i * 125).ljust(6, "0"))
57+
offset = datetime.timedelta(days=i + 1,
58+
seconds=i * 3,
59+
microseconds=microseconds)
60+
nullable_col = base_date + offset
61+
else:
62+
nullable_col = None
63+
data_tuple = (i, col, nullable_col)
64+
self.raw_data.append(data_tuple)
65+
self.data_by_key[i] = data_tuple
66+
67+
def test_4800_bind_timestamp(self):
68+
"4800 - test binding in a timestamp"
69+
self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_LTZ)
70+
self.cursor.execute("""
71+
select * from TestTimestampLTZs
72+
where TimestampLTZCol = :value""",
73+
value=datetime.datetime(2022, 6, 6, 18, 30, 10, 250000))
74+
self.assertEqual(self.cursor.fetchall(), [self.data_by_key[5]])
75+
76+
def test_4801_bind_null(self):
77+
"4801 - test binding in a null"
78+
self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_LTZ)
79+
self.cursor.execute("""
80+
select * from TestTimestampLTZs
81+
where TimestampLTZCol = :value""",
82+
value=None)
83+
self.assertEqual(self.cursor.fetchall(), [])
84+
85+
def test_4802_bind_out_set_input_sizes(self):
86+
"4802 - test binding out with set input sizes defined"
87+
bv = self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_LTZ)
88+
self.cursor.execute("""
89+
begin
90+
:value := to_timestamp('20220603', 'YYYYMMDD');
91+
end;""")
92+
self.assertEqual(bv["value"].getvalue(), datetime.datetime(2022, 6, 3))
93+
94+
def test_4803_bind_in_out_set_input_sizes(self):
95+
"4803 - test binding in/out with set input sizes defined"
96+
bv = self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_LTZ)
97+
self.cursor.execute("""
98+
begin
99+
:value := :value + 5.25;
100+
end;""",
101+
value=datetime.datetime(2022, 5, 10, 12, 0, 0))
102+
self.assertEqual(bv["value"].getvalue(),
103+
datetime.datetime(2022, 5, 15, 18, 0, 0))
104+
105+
def test_4804_bind_out_var(self):
106+
"4804 - test binding out with cursor.var() method"
107+
var = self.cursor.var(oracledb.DB_TYPE_TIMESTAMP_LTZ)
108+
self.cursor.execute("""
109+
begin
110+
:value := to_date('20220601 15:38:12',
111+
'YYYYMMDD HH24:MI:SS');
112+
end;""",
113+
value=var)
114+
self.assertEqual(var.getvalue(),
115+
datetime.datetime(2022, 6, 1, 15, 38, 12))
116+
117+
def test_4805_bind_in_out_var_direct_set(self):
118+
"4805 - test binding in/out with cursor.var() method"
119+
var = self.cursor.var(oracledb.DB_TYPE_TIMESTAMP_LTZ)
120+
var.setvalue(0, datetime.datetime(2022, 5, 30, 6, 0, 0))
121+
self.cursor.execute("""
122+
begin
123+
:value := :value + 5.25;
124+
end;""",
125+
value = var)
126+
self.assertEqual(var.getvalue(),
127+
datetime.datetime(2022, 6, 4, 12, 0, 0))
128+
129+
def test_4806_cursor_description(self):
130+
"4806 - test cursor description is accurate"
131+
self.cursor.execute("select * from TestTimestampLTZs")
132+
expected_value = [
133+
('INTCOL', oracledb.DB_TYPE_NUMBER, 10, None, 9, 0, False),
134+
('TIMESTAMPLTZCOL', oracledb.DB_TYPE_TIMESTAMP_LTZ, 23, None, 0, 6,
135+
False),
136+
('NULLABLECOL', oracledb.DB_TYPE_TIMESTAMP_LTZ, 23, None, 0, 6,
137+
True)
138+
]
139+
self.assertEqual(self.cursor.description, expected_value)
140+
141+
def test_4807_fetchall(self):
142+
"4807 - test that fetching all of the data returns the correct results"
143+
self.cursor.execute("select * From TestTimestampLTZs order by IntCol")
144+
self.assertEqual(self.cursor.fetchall(), self.raw_data)
145+
self.assertEqual(self.cursor.fetchall(), [])
146+
147+
def test_4808_fetchmany(self):
148+
"4808 - test that fetching data in chunks returns the correct results"
149+
self.cursor.execute("select * From TestTimestampLTZs order by IntCol")
150+
self.assertEqual(self.cursor.fetchmany(3), self.raw_data[0:3])
151+
self.assertEqual(self.cursor.fetchmany(2), self.raw_data[3:5])
152+
self.assertEqual(self.cursor.fetchmany(4), self.raw_data[5:9])
153+
self.assertEqual(self.cursor.fetchmany(3), self.raw_data[9:])
154+
self.assertEqual(self.cursor.fetchmany(3), [])
155+
156+
def test_4809_fetchone(self):
157+
"4809 - test that fetching a single row returns the correct results"
158+
self.cursor.execute("""
159+
select *
160+
from TestTimestampLTZs
161+
where IntCol in (3, 4)
162+
order by IntCol""")
163+
self.assertEqual(self.cursor.fetchone(), self.data_by_key[3])
164+
self.assertEqual(self.cursor.fetchone(), self.data_by_key[4])
165+
self.assertEqual(self.cursor.fetchone(), None)
166+
167+
def test_4810_bind_timestamp_with_zero_fseconds(self):
168+
"4810 - test binding a timestamp with zero fractional seconds"
169+
self.cursor.setinputsizes(value=oracledb.DB_TYPE_TIMESTAMP_LTZ)
170+
self.cursor.execute("""
171+
select *
172+
from TestTimestampLTZs
173+
where trunc(TimestampLTZCol) = :value""",
174+
value=datetime.datetime(2022, 6, 12))
175+
self.assertEqual(self.cursor.fetchall(), [self.data_by_key[10]])
176+
177+
if __name__ == "__main__":
178+
test_env.run_test_cases()

0 commit comments

Comments
 (0)