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
17 changes: 15 additions & 2 deletions pandas/io/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -2544,6 +2544,11 @@ def _quote_identifier(name: object) -> str:
return '"' + uname.replace('"', '""') + '"'


# Guard to avoid overwriting user-registered SQLite adapters/converters
# on every to_sql() call. See GH#64337.
_SQLITE_ADAPTERS_REGISTERED: bool = False


class SQLiteTable(SQLTable):
"""
Patch the SQLTable for fallback support.
Expand All @@ -2556,10 +2561,16 @@ def __init__(self, *args, **kwargs) -> None:
self._register_date_adapters()

def _register_date_adapters(self) -> None:
# GH 8341
# register an adapter callable for datetime.time object
# GH 8341 — register adapter callable for datetime.time object
# GH 61092 — register adapters for date/datetime (Python 3.12+)
# GH 64337 — only register once per process to avoid overwriting
# user-registered adapters on subsequent to_sql() calls.
import sqlite3

global _SQLITE_ADAPTERS_REGISTERED
if _SQLITE_ADAPTERS_REGISTERED:
return

# this will transform time(12,34,56,789) into '12:34:56.000789'
# (this is what sqlalchemy does)
def _adapt_time(t) -> str:
Expand All @@ -2584,6 +2595,8 @@ def _adapt_time(t) -> str:
sqlite3.register_converter("date", convert_date)
sqlite3.register_converter("timestamp", convert_timestamp)

_SQLITE_ADAPTERS_REGISTERED = True

def sql_schema(self) -> str:
return str(";\n".join(self.table))

Expand Down
26 changes: 26 additions & 0 deletions pandas/tests/io/test_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -4417,3 +4417,29 @@ def test_xsqlite_if_exists(sqlite_buildin):
(5, "E"),
]
drop_table(table_name, sqlite_buildin)


def test_to_sql_preserves_user_sqlite_adapters(sqlite_buildin):
"""
GH#64337 — DataFrame.to_sql() should not overwrite user-registered
SQLite adapters on subsequent calls.
"""
import sqlite3

# First to_sql call — pandas registers its adapters
df = DataFrame({"t": [time(12, 30)]})
df.to_sql("test_adapter_1", sqlite_buildin, if_exists="replace", index=False)

# User registers a custom adapter for time
def custom_time_adapter(t):
return f"CUSTOM:{t.hour:02d}:{t.minute:02d}:{t.second:02d}"

sqlite3.register_adapter(time, custom_time_adapter)

# Second to_sql call — must NOT overwrite the user's adapter
df2 = DataFrame({"t": [time(14, 0)]})
df2.to_sql("test_adapter_2", sqlite_buildin, if_exists="replace", index=False)

# Verify user's adapter is still registered
registered = sqlite3.adapters.get((time, sqlite3.PrepareProtocol))
assert registered is custom_time_adapter