Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions jaydebeapiarrow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,11 +584,12 @@ def _to_java(p):
return jpype.JArray(jpype.JByte)(p)
if isinstance(p, datetime.datetime):
return jpype.JClass("java.sql.Timestamp").valueOf(
p.strftime("%Y-%m-%d %H:%M:%S"))
p.strftime("%Y-%m-%d %H:%M:%S.%f"))
if isinstance(p, datetime.date):
return jpype.JClass("java.sql.Date").valueOf(p.isoformat())
if isinstance(p, datetime.time):
return jpype.JClass("java.sql.Time").valueOf(p.isoformat())
return jpype.JClass("java.sql.Time").valueOf(
p.strftime("%H:%M:%S"))
if isinstance(p, Decimal):
return jpype.JClass("java.math.BigDecimal")(str(p))
if isinstance(p, list):
Expand Down
43 changes: 43 additions & 0 deletions test/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,45 @@ def test_execute_param_none(self):
result = cursor.fetchone()
self.assertIsNone(result[0])

def test_execute_param_datetime(self):
"""Verify Python datetime objects round-trip correctly via parameter binding."""
stmt = ("insert into ACCOUNT "
"(ACCOUNT_ID, ACCOUNT_NO, BALANCE, OPENED_AT, OPENED_AT_TIME) "
"values (?, ?, ?, ?, ?)")
ts = datetime(2024, 6, 15, 10, 30, 45, 123456)
d = datetime(2024, 6, 15).date()
t = datetime(2024, 6, 15, 10, 30, 45).time()
with self.conn.cursor() as cursor:
cursor.execute(stmt, (ts, 40, Decimal('7.0'), d, t))
cursor.execute(
"select ACCOUNT_ID, OPENED_AT, OPENED_AT_TIME "
"from ACCOUNT where ACCOUNT_NO = 40")
result = cursor.fetchone()
# Timestamp: must match at least to second precision.
# Some drivers (Trino) truncate to milliseconds; Oracle may drop
# fractional seconds. Compare the floor to whole seconds.
self.assertEqual(result[0].replace(microsecond=0),
datetime(2024, 6, 15, 10, 30, 45))
# Date: some drivers (Oracle) return datetime(2024,6,15,0,0) for
# DATE columns; accept both forms.
actual_date = result[1]
if isinstance(actual_date, datetime):
actual_date = actual_date.replace(hour=0, minute=0, second=0,
microsecond=0)
self.assertEqual(actual_date, datetime(2024, 6, 15))
else:
self.assertEqual(actual_date, datetime(2024, 6, 15).date())
# Time: some drivers (Oracle) return datetime(1970,1,1,HH,MM,SS)
# instead of a pure time object; accept both forms.
actual_time = result[2]
if isinstance(actual_time, datetime):
self.assertEqual(actual_time.hour, 10)
self.assertEqual(actual_time.minute, 30)
self.assertEqual(actual_time.second, 45)
else:
self.assertEqual(actual_time.replace(microsecond=0),
datetime(2024, 6, 15, 10, 30, 45).time())

class SqliteTestBase(IntegrationTestBase):

def setUpSql(self):
Expand Down Expand Up @@ -1780,6 +1819,10 @@ def test_execute_param_none(self):
"""Drill has no INSERT INTO ... VALUES — skip param none test."""
self.skipTest("Drill does not support INSERT INTO ... VALUES")

def test_execute_param_datetime(self):
"""Drill has no parameterized INSERT — skip datetime param test."""
self.skipTest("Drill does not support parameterized INSERT queries")

def test_execute_different_rowcounts(self):
"""Drill has no INSERT INTO ... VALUES — skip rowcount test."""
self.skipTest("Drill does not support INSERT INTO ... VALUES")
Expand Down
42 changes: 42 additions & 0 deletions test/test_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,36 @@ def test_to_java_datetime(self):
self.assertEqual(len(captured), 1)
self.assertIsInstance(captured[0][1], Timestamp)

def test_to_java_datetime_preserves_microseconds(self):
"""datetime with microseconds should preserve fractional seconds in Timestamp."""
import jpype
Timestamp = jpype.JClass("java.sql.Timestamp")
dt = datetime(2024, 6, 15, 10, 30, 45, 123456)
self.conn.jconn.mockSetObjectCapture()
with self.conn.cursor() as cursor:
cursor.execute("dummy stmt", (dt,))
captured = self.conn.jconn.getCapturedSetObjectArgs()
self.assertEqual(len(captured), 1)
self.assertIsInstance(captured[0][1], Timestamp)
ts = captured[0][1]
self.assertEqual(ts.getNanos(), 123456000)

def test_to_java_datetime_mixed_params(self):
"""datetime alongside other types should all convert correctly."""
import jpype
Timestamp = jpype.JClass("java.sql.Timestamp")
dt = datetime(2024, 1, 2, 3, 4, 5, 500000)
self.conn.jconn.mockSetObjectCapture()
with self.conn.cursor() as cursor:
cursor.execute("dummy stmt", (42, "hello", dt, None))
captured = self.conn.jconn.getCapturedSetObjectArgs()
# None uses setNull() (not setObject), so only 3 captures
self.assertEqual(len(captured), 3)
self.assertEqual(captured[0][1], 42)
self.assertEqual(captured[1][1], "hello")
self.assertIsInstance(captured[2][1], Timestamp)
self.assertEqual(captured[2][1].getNanos(), 500000000)

def test_to_java_date(self):
"""date should convert to java.sql.Date."""
import jpype
Expand All @@ -670,6 +700,18 @@ def test_to_java_time(self):
self.assertEqual(len(captured), 1)
self.assertIsInstance(captured[0][1], Time)

def test_to_java_time_with_microseconds(self):
"""time with microseconds should convert to java.sql.Time without error."""
import jpype
Time = jpype.JClass("java.sql.Time")
t = datetime(2024, 6, 15, 10, 30, 45, 999999).time()
self.conn.jconn.mockSetObjectCapture()
with self.conn.cursor() as cursor:
cursor.execute("dummy stmt", (t,))
captured = self.conn.jconn.getCapturedSetObjectArgs()
self.assertEqual(len(captured), 1)
self.assertIsInstance(captured[0][1], Time)

def test_to_java_decimal(self):
"""Decimal should convert to java.math.BigDecimal."""
import jpype
Expand Down
Loading