Skip to content

Commit 322e5bf

Browse files
committed
fix: Date→datetime and Boolean→int via colspecs TypeDecorator
Previous output_converter approach didn't work because the GaussDB ODBC driver already returns Python objects (datetime/int), not raw bytes — pyodbc's add_output_converter only fires on raw bytes. New approach: register _GaussDBOdbcDate and _GaussDBOdbcBoolean as TypeDecorator subclasses in both colspecs (for user-defined columns) and ischema_names (for reflected columns), ensuring result processors fire on ALL Date/Boolean columns regardless of how they were defined.
1 parent 976f59b commit 322e5bf

2 files changed

Lines changed: 67 additions & 4 deletions

File tree

src/gaussdb_sqlalchemy/base.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
from sqlalchemy.dialects.postgresql.base import PGDDLCompiler
1111
from sqlalchemy.dialects.postgresql.base import PGIdentifierPreparer
1212
from sqlalchemy.dialects.postgresql.base import PGTypeCompiler
13+
from sqlalchemy.dialects.postgresql.base import PGExecutionContext
14+
from sqlalchemy.dialects.postgresql import DATE as _PG_DATE
15+
from sqlalchemy.dialects.postgresql import BOOLEAN as _PG_BOOLEAN
1316
from sqlalchemy.exc import NoSuchTableError
1417
from sqlalchemy import schema as sa_schema
1518
from sqlalchemy import types as sqltypes
@@ -19,6 +22,56 @@
1922
from sqlalchemy.sql import operators
2023
from sqlalchemy.sql.compiler import OPERATORS
2124

25+
import datetime as _dt
26+
27+
28+
class _GaussDBOdbcDate(sqltypes.TypeDecorator):
29+
"""Date type that normalises ODBC driver datetime returns to date.
30+
31+
The GaussDB ODBC driver on Windows returns ``datetime.datetime``
32+
for DATE columns instead of ``datetime.date``. pyodbc's
33+
``add_output_converter`` cannot fix this because the driver already
34+
converted the raw bytes to a Python object before pyodbc sees it.
35+
36+
This TypeDecorator wraps the standard PG DATE type and adds a
37+
result processor that strips the time component.
38+
"""
39+
40+
impl = _PG_DATE
41+
cache_ok = True
42+
43+
def process_result_value(self, value, dialect):
44+
if value is None:
45+
return value
46+
if isinstance(value, _dt.datetime):
47+
return value.date()
48+
if isinstance(value, _dt.date):
49+
return value
50+
return value
51+
52+
53+
class _GaussDBOdbcBoolean(sqltypes.TypeDecorator):
54+
"""Boolean type that normalises ODBC driver integer returns to bool.
55+
56+
The GaussDB ODBC driver returns ``int`` (1/0) for boolean columns.
57+
SQLAlchemy's default ``Boolean.result_processor`` is a no-op when
58+
``supports_native_boolean`` is True, so we must add our own.
59+
"""
60+
61+
impl = _PG_BOOLEAN
62+
cache_ok = True
63+
64+
def process_result_value(self, value, dialect):
65+
if value is None:
66+
return value
67+
if isinstance(value, bool):
68+
return value
69+
if isinstance(value, (bytes, bytearray)):
70+
value = value.decode("utf-8")
71+
if isinstance(value, str):
72+
return value.strip().lower() in ("1", "t", "true", "y")
73+
return bool(value)
74+
2275

2376
class GaussDBCompiler(PGCompiler):
2477
def visit_concat_op_binary(self, binary, operator_, **kw):
@@ -173,9 +226,6 @@ def visit_large_binary(self, type_, **kw):
173226
return super().visit_large_binary(type_, **kw)
174227

175228

176-
from sqlalchemy.dialects.postgresql.base import PGExecutionContext
177-
178-
179229
class GaussDBMExecutionContext(PGExecutionContext):
180230
def get_lastrowid(self):
181231
try:
@@ -218,9 +268,13 @@ class GaussDBDialect(PGDialect):
218268
gaussdb_compatibility = None
219269

220270
# Register GaussDB M-compat binary types for reflection.
271+
# Override date/boolean with ODBC-aware variants that normalise
272+
# driver returns (datetime→date, int→bool).
221273
ischema_names = dict(PGDialect.ischema_names)
222274
ischema_names["blob"] = LargeBinary
223275
ischema_names["longblob"] = LargeBinary
276+
ischema_names["date"] = _GaussDBOdbcDate
277+
ischema_names["boolean"] = _GaussDBOdbcBoolean
224278

225279
def initialize(self, connection):
226280
super().initialize(connection)

src/gaussdb_sqlalchemy/odbc.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,19 @@
2323

2424
from . import odbc_dbapi
2525
from .alembic import register_alembic_impl
26-
from .base import GaussDBDialect
26+
from .base import GaussDBDialect, _GaussDBOdbcDate, _GaussDBOdbcBoolean
2727

2828

2929
register_alembic_impl()
3030

31+
32+
# Inherit PG colspecs and override Date/Boolean with ODBC-aware variants.
33+
# This ensures that BOTH user-defined Column(Date) and reflected columns
34+
# get the result processors that normalise ODBC driver returns.
35+
_cspecs = dict(GaussDBDialect.colspecs)
36+
_cspecs[sqltypes.Date] = _GaussDBOdbcDate
37+
_cspecs[sqltypes.Boolean] = _GaussDBOdbcBoolean
38+
3139
# Query-string keys that are not forwarded as ODBC connection attributes.
3240
_CONTROL_KEYS = {"driver", "dsn"}
3341

@@ -38,6 +46,7 @@ class GaussDBDialect_odbc(GaussDBDialect):
3846
driver = "odbc"
3947
default_paramstyle = "qmark"
4048
supports_statement_cache = True
49+
colspecs = _cspecs
4150

4251
@classmethod
4352
def import_dbapi(cls):

0 commit comments

Comments
 (0)