Skip to content

Commit 19dbf7d

Browse files
committed
SA20: Restore backward-compatibility with SQLAlchemy 1.3
Note that this is on a best-effort basis. SQLAlchemy 2.0 will be mostly compatible with most SQLAlchemy 1.3 code which is not using any of the deprecated functionalities, modulo a few aspects. > It is possible to have code that is compatible with 2.0 and 1.3 at the > same time, you would just need to ensure you use the subset of > features and APIs that are common to both. This patch adds a small compatibility layer, which activates on SA13 only, and monkey-patches two spots where anomalies have been spotted.
1 parent 5bff9d2 commit 19dbf7d

File tree

4 files changed

+147
-0
lines changed

4 files changed

+147
-0
lines changed

src/crate/client/sqlalchemy/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@
1919
# with Crate these terms will supersede the license and you may use the
2020
# software solely pursuant to the terms of the relevant commercial agreement.
2121

22+
from .compat.api13 import monkeypatch_add_exec_driver_sql
2223
from .dialect import CrateDialect
24+
from .sa_version import SA_1_4, SA_VERSION
25+
26+
# SQLAlchemy 1.3 does not have the `exec_driver_sql` method.
27+
if SA_VERSION < SA_1_4:
28+
monkeypatch_add_exec_driver_sql()
2329

2430
__all__ = [
2531
CrateDialect,

src/crate/client/sqlalchemy/compat/__init__.py

Whitespace-only changes.
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# -*- coding: utf-8; -*-
2+
#
3+
# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
4+
# license agreements. See the NOTICE file distributed with this work for
5+
# additional information regarding copyright ownership. Crate licenses
6+
# this file to you under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License. You may
8+
# obtain a copy of the License at
9+
#
10+
# https://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
# License for the specific language governing permissions and limitations
16+
# under the License.
17+
#
18+
# However, if you have executed another commercial license agreement
19+
# with Crate these terms will supersede the license and you may use the
20+
# software solely pursuant to the terms of the relevant commercial agreement.
21+
22+
"""
23+
Compatibility module for running a subset of SQLAlchemy 2.0 programs on
24+
SQLAlchemy 1.3. By using monkey-patching, it can do two things:
25+
26+
1. Add the `exec_driver_sql` method to SA's `Connection` and `Engine`.
27+
2. Amend the `sql.select` function to accept the calling semantics of
28+
the modern variant.
29+
30+
Reason: `exec_driver_sql` gets used within the CrateDB dialect already,
31+
and the new calling semantics of `sql.select` already get used within
32+
many of the test cases already. Please note that the patch for
33+
`sql.select` is only applied when running the test suite.
34+
"""
35+
36+
import collections.abc as collections_abc
37+
38+
from sqlalchemy import exc
39+
from sqlalchemy.sql import Select
40+
from sqlalchemy.sql import select as original_select
41+
from sqlalchemy.util import immutabledict
42+
43+
44+
# `_distill_params_20` copied from SA14's `sqlalchemy.engine.{base,util}`.
45+
_no_tuple = ()
46+
_no_kw = immutabledict()
47+
48+
49+
def _distill_params_20(params):
50+
if params is None:
51+
return _no_tuple, _no_kw
52+
elif isinstance(params, list):
53+
# collections_abc.MutableSequence): # avoid abc.__instancecheck__
54+
if params and not isinstance(params[0], (collections_abc.Mapping, tuple)):
55+
raise exc.ArgumentError(
56+
"List argument must consist only of tuples or dictionaries"
57+
)
58+
59+
return (params,), _no_kw
60+
elif isinstance(
61+
params,
62+
(tuple, dict, immutabledict),
63+
# only do abc.__instancecheck__ for Mapping after we've checked
64+
# for plain dictionaries and would otherwise raise
65+
) or isinstance(params, collections_abc.Mapping):
66+
return (params,), _no_kw
67+
else:
68+
raise exc.ArgumentError("mapping or sequence expected for parameters")
69+
70+
71+
def exec_driver_sql(self, statement, parameters=None, execution_options=None):
72+
"""
73+
Adapter for `exec_driver_sql`, which is available since SA14, for SA13.
74+
"""
75+
if execution_options is not None:
76+
raise ValueError(
77+
"SA13 backward-compatibility: "
78+
"`exec_driver_sql` does not support `execution_options`"
79+
)
80+
args_10style, kwargs_10style = _distill_params_20(parameters)
81+
return self.execute(statement, *args_10style, **kwargs_10style)
82+
83+
84+
def monkeypatch_add_exec_driver_sql():
85+
"""
86+
Transparently add SA14's `exec_driver_sql()` method to SA13.
87+
88+
AttributeError: 'Connection' object has no attribute 'exec_driver_sql'
89+
AttributeError: 'Engine' object has no attribute 'exec_driver_sql'
90+
"""
91+
from sqlalchemy.engine.base import Connection, Engine
92+
93+
# Add `exec_driver_sql` method to SA's `Connection` and `Engine` classes.
94+
Connection.exec_driver_sql = exec_driver_sql
95+
Engine.exec_driver_sql = exec_driver_sql
96+
97+
98+
def select_sa14(*columns, **kw) -> Select:
99+
"""
100+
Adapt SA14/SA20's calling semantics of `sql.select()` to SA13.
101+
102+
With SA20, `select()` no longer accepts varied constructor arguments, only
103+
the "generative" style of `select()` will be supported. The list of columns
104+
/ tables to select from should be passed positionally.
105+
106+
Derived from https://github.com/sqlalchemy/alembic/blob/b1fad6b6/alembic/util/sqla_compat.py#L557-L558
107+
108+
sqlalchemy.exc.ArgumentError: columns argument to select() must be a Python list or other iterable
109+
"""
110+
if isinstance(columns, tuple) and isinstance(columns[0], list):
111+
if "whereclause" in kw:
112+
raise ValueError(
113+
"SA13 backward-compatibility: "
114+
"`whereclause` is both in kwargs and columns tuple"
115+
)
116+
columns, whereclause = columns
117+
kw["whereclause"] = whereclause
118+
return original_select(columns, **kw)
119+
120+
121+
def monkeypatch_amend_select_sa14():
122+
"""
123+
Make SA13's `sql.select()` transparently accept calling semantics of SA14
124+
and SA20, by swapping in the newer variant of `select_sa14()`.
125+
126+
This supports the test suite of `crate-python`, because it already uses the
127+
modern calling semantics.
128+
"""
129+
import sqlalchemy
130+
131+
sqlalchemy.select = select_sa14
132+
sqlalchemy.sql.select = select_sa14
133+
sqlalchemy.sql.expression.select = select_sa14

src/crate/client/sqlalchemy/tests/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# -*- coding: utf-8 -*-
22

3+
from ..compat.api13 import monkeypatch_amend_select_sa14
4+
from ..sa_version import SA_1_4, SA_VERSION
5+
6+
# `sql.select()` of SQLAlchemy 1.3 uses old calling semantics,
7+
# but the test cases already need the modern ones.
8+
if SA_VERSION < SA_1_4:
9+
monkeypatch_amend_select_sa14()
10+
311
from unittest import TestSuite, makeSuite
412
from .connection_test import SqlAlchemyConnectionTest
513
from .dict_test import SqlAlchemyDictTypeTest

0 commit comments

Comments
 (0)