Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SQLAlchemy 2.0: Restore backward-compatibility with SQLAlchemy 1.3 #497

Merged
merged 2 commits into from
Dec 23, 2022
Merged
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
6 changes: 5 additions & 1 deletion docs/by-example/sqlalchemy/advanced-querying.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ Introduction
Import the relevant symbols:

>>> import sqlalchemy as sa
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> from sqlalchemy.orm import sessionmaker
>>> try:
... from sqlalchemy.orm import declarative_base
... except ImportError:
... from sqlalchemy.ext.declarative import declarative_base
>>> from uuid import uuid4

Establish a connection to the database, see also :ref:`sa:engines_toplevel`
Expand Down
6 changes: 5 additions & 1 deletion docs/by-example/sqlalchemy/crud.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ Import the relevant symbols:
>>> import sqlalchemy as sa
>>> from datetime import datetime
>>> from sqlalchemy import delete, func, text
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> from sqlalchemy.orm import sessionmaker
>>> try:
... from sqlalchemy.orm import declarative_base
... except ImportError:
... from sqlalchemy.ext.declarative import declarative_base
>>> from crate.client.sqlalchemy.types import ObjectArray

Establish a connection to the database:
Expand Down
6 changes: 5 additions & 1 deletion docs/by-example/sqlalchemy/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ Introduction
Import the relevant symbols:

>>> import sqlalchemy as sa
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> from sqlalchemy.orm import sessionmaker
>>> try:
... from sqlalchemy.orm import declarative_base
... except ImportError:
... from sqlalchemy.ext.declarative import declarative_base

Establish a connection to the database, see also :ref:`sa:engines_toplevel`
and :ref:`connect`:
Expand Down
6 changes: 5 additions & 1 deletion docs/by-example/sqlalchemy/working-with-types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ Import the relevant symbols:
>>> from datetime import datetime
>>> from geojson import Point, Polygon
>>> from sqlalchemy import delete, func, text
>>> from sqlalchemy.orm import declarative_base, sessionmaker
>>> from sqlalchemy.orm import sessionmaker
>>> from sqlalchemy.sql import operators
>>> try:
... from sqlalchemy.orm import declarative_base
... except ImportError:
... from sqlalchemy.ext.declarative import declarative_base
>>> from uuid import uuid4
>>> from crate.client.sqlalchemy.types import Object, ObjectArray
>>> from crate.client.sqlalchemy.types import Geopoint, Geoshape
Expand Down
6 changes: 6 additions & 0 deletions src/crate/client/sqlalchemy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
# with Crate these terms will supersede the license and you may use the
# software solely pursuant to the terms of the relevant commercial agreement.

from .compat.api13 import monkeypatch_add_exec_driver_sql
from .dialect import CrateDialect
from .sa_version import SA_1_4, SA_VERSION

# SQLAlchemy 1.3 does not have the `exec_driver_sql` method.
if SA_VERSION < SA_1_4:
monkeypatch_add_exec_driver_sql()

__all__ = [
CrateDialect,
Expand Down
Empty file.
133 changes: 133 additions & 0 deletions src/crate/client/sqlalchemy/compat/api13.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# -*- coding: utf-8; -*-
#
# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
# license agreements. See the NOTICE file distributed with this work for
# additional information regarding copyright ownership. Crate licenses
# this file to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may
# obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# However, if you have executed another commercial license agreement
# with Crate these terms will supersede the license and you may use the
# software solely pursuant to the terms of the relevant commercial agreement.

"""
Compatibility module for running a subset of SQLAlchemy 2.0 programs on
SQLAlchemy 1.3. By using monkey-patching, it can do two things:

1. Add the `exec_driver_sql` method to SA's `Connection` and `Engine`.
2. Amend the `sql.select` function to accept the calling semantics of
the modern variant.

Reason: `exec_driver_sql` gets used within the CrateDB dialect already,
and the new calling semantics of `sql.select` already get used within
many of the test cases already. Please note that the patch for
`sql.select` is only applied when running the test suite.
"""

import collections.abc as collections_abc

from sqlalchemy import exc
from sqlalchemy.sql import Select
from sqlalchemy.sql import select as original_select
from sqlalchemy.util import immutabledict


# `_distill_params_20` copied from SA14's `sqlalchemy.engine.{base,util}`.
_no_tuple = ()
_no_kw = immutabledict()


def _distill_params_20(params):
if params is None:
return _no_tuple, _no_kw
elif isinstance(params, list):
# collections_abc.MutableSequence): # avoid abc.__instancecheck__
if params and not isinstance(params[0], (collections_abc.Mapping, tuple)):
raise exc.ArgumentError(
"List argument must consist only of tuples or dictionaries"
)

return (params,), _no_kw
elif isinstance(
params,
(tuple, dict, immutabledict),
# only do abc.__instancecheck__ for Mapping after we've checked
# for plain dictionaries and would otherwise raise
) or isinstance(params, collections_abc.Mapping):
return (params,), _no_kw
else:
raise exc.ArgumentError("mapping or sequence expected for parameters")


def exec_driver_sql(self, statement, parameters=None, execution_options=None):
"""
Adapter for `exec_driver_sql`, which is available since SA14, for SA13.
"""
if execution_options is not None:
raise ValueError(
"SA13 backward-compatibility: "
"`exec_driver_sql` does not support `execution_options`"
)
args_10style, kwargs_10style = _distill_params_20(parameters)
return self.execute(statement, *args_10style, **kwargs_10style)


def monkeypatch_add_exec_driver_sql():
"""
Transparently add SA14's `exec_driver_sql()` method to SA13.

AttributeError: 'Connection' object has no attribute 'exec_driver_sql'
AttributeError: 'Engine' object has no attribute 'exec_driver_sql'
"""
from sqlalchemy.engine.base import Connection, Engine

# Add `exec_driver_sql` method to SA's `Connection` and `Engine` classes.
Connection.exec_driver_sql = exec_driver_sql
Engine.exec_driver_sql = exec_driver_sql


def select_sa14(*columns, **kw) -> Select:
"""
Adapt SA14/SA20's calling semantics of `sql.select()` to SA13.

With SA20, `select()` no longer accepts varied constructor arguments, only
the "generative" style of `select()` will be supported. The list of columns
/ tables to select from should be passed positionally.

Derived from https://github.com/sqlalchemy/alembic/blob/b1fad6b6/alembic/util/sqla_compat.py#L557-L558

sqlalchemy.exc.ArgumentError: columns argument to select() must be a Python list or other iterable
"""
if isinstance(columns, tuple) and isinstance(columns[0], list):
if "whereclause" in kw:
raise ValueError(
"SA13 backward-compatibility: "
"`whereclause` is both in kwargs and columns tuple"
)
columns, whereclause = columns
kw["whereclause"] = whereclause
return original_select(columns, **kw)


def monkeypatch_amend_select_sa14():
"""
Make SA13's `sql.select()` transparently accept calling semantics of SA14
and SA20, by swapping in the newer variant of `select_sa14()`.

This supports the test suite of `crate-python`, because it already uses the
modern calling semantics.
"""
import sqlalchemy

sqlalchemy.select = select_sa14
sqlalchemy.sql.select = select_sa14
sqlalchemy.sql.expression.select = select_sa14
8 changes: 8 additions & 0 deletions src/crate/client/sqlalchemy/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# -*- coding: utf-8 -*-

from ..compat.api13 import monkeypatch_amend_select_sa14
from ..sa_version import SA_1_4, SA_VERSION

# `sql.select()` of SQLAlchemy 1.3 uses old calling semantics,
# but the test cases already need the modern ones.
if SA_VERSION < SA_1_4:
monkeypatch_amend_select_sa14()

from unittest import TestSuite, makeSuite
from .connection_test import SqlAlchemyConnectionTest
from .dict_test import SqlAlchemyDictTypeTest
Expand Down
6 changes: 5 additions & 1 deletion src/crate/client/sqlalchemy/tests/array_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@

import sqlalchemy as sa
from sqlalchemy.sql import operators
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy.orm import Session
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base

from crate.client.cursor import Cursor

Expand Down
6 changes: 5 additions & 1 deletion src/crate/client/sqlalchemy/tests/bulk_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
from unittest.mock import patch, MagicMock

import sqlalchemy as sa
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy.orm import Session
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base

from crate.client.cursor import Cursor

Expand Down
5 changes: 4 additions & 1 deletion src/crate/client/sqlalchemy/tests/create_table_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
# software solely pursuant to the terms of the relevant commercial agreement.

import sqlalchemy as sa
from sqlalchemy.orm import declarative_base
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base

from crate.client.sqlalchemy.types import Object, ObjectArray, Geopoint
from crate.client.cursor import Cursor
Expand Down
6 changes: 5 additions & 1 deletion src/crate/client/sqlalchemy/tests/datetime_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@

import sqlalchemy as sa
from sqlalchemy.exc import DBAPIError
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy.orm import Session
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base

from crate.client.cursor import Cursor

Expand Down
6 changes: 5 additions & 1 deletion src/crate/client/sqlalchemy/tests/dialect_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@
from crate.client.cursor import Cursor
from crate.client.sqlalchemy.types import Object
from sqlalchemy import inspect
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy.orm import Session
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.testing import eq_, in_

FakeCursor = MagicMock(name='FakeCursor', spec=Cursor)
Expand Down
6 changes: 5 additions & 1 deletion src/crate/client/sqlalchemy/tests/dict_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@

import sqlalchemy as sa
from sqlalchemy.sql import select
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy.orm import Session
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base

from crate.client.sqlalchemy.types import Craty, ObjectArray
from crate.client.cursor import Cursor
Expand Down
5 changes: 4 additions & 1 deletion src/crate/client/sqlalchemy/tests/function_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@
from unittest import TestCase

import sqlalchemy as sa
from sqlalchemy.orm import declarative_base
from sqlalchemy.sql.sqltypes import TIMESTAMP
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base


class SqlAlchemyFunctionTest(TestCase):
Expand Down
6 changes: 5 additions & 1 deletion src/crate/client/sqlalchemy/tests/insert_from_select_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@
from unittest.mock import patch, MagicMock

import sqlalchemy as sa
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy import select, insert
from sqlalchemy.orm import Session
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base

from crate.client.cursor import Cursor

Expand Down
6 changes: 5 additions & 1 deletion src/crate/client/sqlalchemy/tests/match_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
from unittest.mock import MagicMock

import sqlalchemy as sa
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy.orm import Session
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base

from crate.client.sqlalchemy.types import Craty
from crate.client.sqlalchemy.predicates import match
Expand Down
6 changes: 5 additions & 1 deletion src/crate/client/sqlalchemy/tests/update_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
from crate.client.sqlalchemy.types import Object

import sqlalchemy as sa
from sqlalchemy.orm import declarative_base, Session
from sqlalchemy.orm import Session
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base

from crate.client.cursor import Cursor

Expand Down