Skip to content

Commit fca070e

Browse files
fix: Retry SQLAlchemy engine creation for adapters without JSON SerDe support (#1949)
* fix: Retry SQLAlchemy engine creation for adapters without JSON SerDe support * Add test * Fix for SQLAlchemy 1
1 parent 0fb0c1b commit fca070e

File tree

4 files changed

+67
-7
lines changed

4 files changed

+67
-7
lines changed

samples/sample_custom_sql_adapter/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
import typing as t
4+
5+
from sqlalchemy.engine.default import DefaultDialect
6+
7+
if t.TYPE_CHECKING:
8+
from types import ModuleType
9+
10+
11+
class CustomSQLDialect(DefaultDialect):
12+
"""Custom SQLite dialect that supports JSON."""
13+
14+
name = "myrdbms"
15+
16+
def __init__(self, *args, **kwargs):
17+
super().__init__(*args, **kwargs)
18+
19+
@classmethod
20+
def import_dbapi(cls):
21+
"""Import the sqlite3 DBAPI."""
22+
import sqlite3
23+
24+
return sqlite3
25+
26+
@classmethod
27+
def dbapi(cls) -> ModuleType: # type: ignore[override]
28+
"""Return the DBAPI module.
29+
30+
NOTE: This is a legacy method that will stop being used by SQLAlchemy at some point.
31+
""" # noqa: E501
32+
return cls.import_dbapi()

singer_sdk/connectors/sql.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -319,12 +319,21 @@ def create_engine(self) -> Engine:
319319
Returns:
320320
A new SQLAlchemy Engine.
321321
"""
322-
return sqlalchemy.create_engine(
323-
self.sqlalchemy_url,
324-
echo=False,
325-
json_serializer=self.serialize_json,
326-
json_deserializer=self.deserialize_json,
327-
)
322+
try:
323+
return sqlalchemy.create_engine(
324+
self.sqlalchemy_url,
325+
echo=False,
326+
json_serializer=self.serialize_json,
327+
json_deserializer=self.deserialize_json,
328+
)
329+
except TypeError:
330+
self.logger.exception(
331+
"Retrying engine creation with fewer arguments due to TypeError.",
332+
)
333+
return sqlalchemy.create_engine(
334+
self.sqlalchemy_url,
335+
echo=False,
336+
)
328337

329338
def quote(self, name: str) -> str:
330339
"""Quote a name if it needs quoting, using '.' as a name-part delimiter.

tests/core/test_connector_sql.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
from __future__ import annotations
22

3+
import typing as t
34
from decimal import Decimal
45
from unittest import mock
56

67
import pytest
78
import sqlalchemy
8-
from sqlalchemy.dialects import sqlite
9+
from sqlalchemy.dialects import registry, sqlite
910

1011
from singer_sdk.connectors import SQLConnector
1112
from singer_sdk.exceptions import ConfigValidationError
1213

14+
if t.TYPE_CHECKING:
15+
from sqlalchemy.engine import Engine
16+
1317

1418
def stringify(in_dict):
1519
return {k: str(v) for k, v in in_dict.items()}
@@ -283,3 +287,18 @@ def test_engine_json_serialization(self, connector: SQLConnector):
283287
(1, {"x": Decimal("1.0")}),
284288
(2, {"x": Decimal("2.0"), "y": [1, 2, 3]}),
285289
]
290+
291+
292+
def test_adapter_without_json_serde():
293+
registry.register(
294+
"myrdbms",
295+
"samples.sample_custom_sql_adapter.connector",
296+
"CustomSQLDialect",
297+
)
298+
299+
class CustomConnector(SQLConnector):
300+
def create_engine(self) -> Engine:
301+
return super().create_engine()
302+
303+
connector = CustomConnector(config={"sqlalchemy_url": "myrdbms:///"})
304+
connector.create_engine()

0 commit comments

Comments
 (0)