Skip to content

Commit a4154bd

Browse files
nsoranzobernt-matthiascnulucventuriniBradenM
authored
SQLAlchemy 1.4 support + Move CI to GitHub workflows (#506)
* fix database existence check follow up to #372 * always dispose engine after db existence check * add docs for postgres_db parameter * fix dialect_name Co-authored-by: Nicola Soranzo <[email protected]> * optimize execution order Co-authored-by: Nicola Soranzo <[email protected]> * database_exists fix return - postgres: return for the first positive test - use immutable for default argument * use None as default Co-authored-by: Nicola Soranzo <[email protected]> * break if successful Co-authored-by: Nicola Soranzo <[email protected]> * dispose only for exception * use Null connection pool and close connections - use a connection (which is closed automatically) for data base existence check - explicitely use Null connection pool already with the 1st change disposal of the engine (which closes all open connections) is not necessary anymore. with the second change we are completely sure. * fix isort call in tox.ini and import order * rename parameter to databases * move functions to module level * Add support for SQLAlchemy 1.4 Import _ColumnEntity from sqlalchemy.orm.context if importing from .query fails. And while checking if database_exists, use url.set() as URL object is now immutable. * Fixed errors related to URL.database not being directly settable. * Removed the try..except constructs to set the database. Now checking *once* the version of sqlalchemy in use and deciding *once* how to change the database address. Using a wrapping function ('set_database_from_url') to simplify the code. * Cleanups * Use `execution_options()` method of `Connection` to set the transaction isolation level to autocommit for PostgreSQL. * Lint fixes * Move CI lint job to GitHub workflows * Testing: drop Python 3.5 and Python 3.9 * Use `with engine.connect()` context manager Consolidate autocommit across drivers that support it, xref. #494 and fix #486 . * Move CI tests from TravisCI to GitHub workflows * Add SQLAlchemy 1.3 on Python 3.6 to the test matrix * Update tests for changes in `create_database` to use `with engine.connect()` context manager (commit 4f52578 ) * Install the version of pg8000 recommended by SQLAlchemy 1.3 when testing. xref. #500 Fix the following exception: ``` _____________________________________________________________________________ TestDatabasePostgresPg8000.test_create_and_drop _____________________________________________________________________________ self = <tests.functions.test_database.TestDatabasePostgresPg8000 object at 0x7f3b3da76490>, dsn = 'postgresql+pg8000://postgres:postgres@localhost/db_to_test_create_and_drop_via_pg8000_driver' def test_create_and_drop(self, dsn): > assert not database_exists(dsn) tests/functions/test_database.py:15: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ sqlalchemy_utils/functions/database.py:488: in database_exists return bool(_get_scalar_result(engine, text)) sqlalchemy_utils/functions/database.py:443: in _get_scalar_result with engine.connect() as conn: .venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py:2263: in connect return self._connection_cls(self, **kwargs) .venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py:104: in __init__ else engine.raw_connection() .venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py:2369: in raw_connection return self._wrap_pool_connect( .venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py:2336: in _wrap_pool_connect return fn() .venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py:304: in unique_connection return _ConnectionFairy._checkout(self) .venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py:778: in _checkout fairy = _ConnectionRecord.checkout(pool) .venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py:495: in checkout rec = pool._do_get() .venv/lib/python3.8/site-packages/sqlalchemy/pool/impl.py:241: in _do_get return self._create_connection() .venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py:309: in _create_connection return _ConnectionRecord(self) .venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py:440: in __init__ self.__connect(first_connect_check=True) .venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py:664: in __connect pool.dispatch.first_connect.for_modify( .venv/lib/python3.8/site-packages/sqlalchemy/event/attr.py:314: in exec_once_unless_exception self._exec_once_impl(True, *args, **kw) .venv/lib/python3.8/site-packages/sqlalchemy/event/attr.py:285: in _exec_once_impl self(*args, **kw) .venv/lib/python3.8/site-packages/sqlalchemy/event/attr.py:322: in __call__ fn(*args, **kw) .venv/lib/python3.8/site-packages/sqlalchemy/util/langhelpers.py:1406: in go return once_fn(*arg, **kw) .venv/lib/python3.8/site-packages/sqlalchemy/engine/strategies.py:199: in first_connect dialect.initialize(c) .venv/lib/python3.8/site-packages/sqlalchemy/dialects/postgresql/pg8000.py:215: in initialize super(PGDialect_pg8000, self).initialize(connection) .venv/lib/python3.8/site-packages/sqlalchemy/dialects/postgresql/base.py:2624: in initialize super(PGDialect, self).initialize(connection) .venv/lib/python3.8/site-packages/sqlalchemy/engine/default.py:311: in initialize self.server_version_info = self._get_server_version_info( .venv/lib/python3.8/site-packages/sqlalchemy/dialects/postgresql/base.py:2869: in _get_server_version_info v = connection.execute("select version()").scalar() .venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py:1003: in execute return self._execute_text(object_, multiparams, params) .venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py:1172: in _execute_text ret = self._execute_context( .venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py:1316: in _execute_context self._handle_dbapi_exception( .venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py:1514: in _handle_dbapi_exception util.raise_(exc_info[1], with_traceback=exc_info[2]) .venv/lib/python3.8/site-packages/sqlalchemy/util/compat.py:182: in raise_ raise exception .venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py:1294: in _execute_context result = context._setup_crud_result_proxy() .venv/lib/python3.8/site-packages/sqlalchemy/engine/default.py:1258: in _setup_crud_result_proxy result = self.get_result_proxy() .venv/lib/python3.8/site-packages/sqlalchemy/engine/default.py:1233: in get_result_proxy return result.ResultProxy(self) .venv/lib/python3.8/site-packages/sqlalchemy/engine/result.py:775: in __init__ self._init_metadata() .venv/lib/python3.8/site-packages/sqlalchemy/engine/result.py:807: in _init_metadata self._metadata = ResultMetaData(self, cursor_description) .venv/lib/python3.8/site-packages/sqlalchemy/engine/result.py:290: in __init__ raw = self._merge_cursor_description( .venv/lib/python3.8/site-packages/sqlalchemy/engine/result.py:496: in _merge_cursor_description return [ .venv/lib/python3.8/site-packages/sqlalchemy/engine/result.py:496: in <listcomp> return [ .venv/lib/python3.8/site-packages/sqlalchemy/engine/result.py:616: in _merge_cols_by_none for ( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <sqlalchemy.engine.result.ResultMetaData object at 0x7f3b3daa4a90>, context = <sqlalchemy.dialects.postgresql.pg8000.PGExecutionContext_pg8000 object at 0x7f3b3dda7100> cursor_description = [('version', 25, None, None, None, None, ...)] def _colnames_from_description(self, context, cursor_description): """Extract column names and data types from a cursor.description. Applies unicode decoding, column translation, "normalization", and case sensitivity rules to the names based on the dialect. """ dialect = context.dialect case_sensitive = dialect.case_sensitive translate_colname = context._translate_colname description_decoder = ( dialect._description_decoder if dialect.description_encoding else None ) normalize_name = ( dialect.normalize_name if dialect.requires_name_normalize else None ) untranslated = None self.keys = [] for idx, rec in enumerate(cursor_description): colname = rec[0] coltype = rec[1] if description_decoder: > colname = description_decoder(colname) E TypeError: expected bytes, str found .venv/lib/python3.8/site-packages/sqlalchemy/engine/result.py:545: TypeError ``` * Fix another import for SQLAlchemy 1.4 * Fix test failing on SQLAlchemy 1.4 * Fix `AttributeError: 'Query' object has no attribute '_entities'` * Fix `AttributeError: type object 'User' has no attribute '_decl_class_registry'` and similar messages for other objects. * Extend linting to all Python files * Fix `AttributeError: 'Query' object has no attribute '_mapper_zero'` * Fix `AttributeError: module 'sqlalchemy.orm.mapper' has no attribute '_mapper_registry'` * Fix `make_order_by_deterministic` for SQLAlchemy 1.4 Fix errors: ``` def make_order_by_deterministic(query): """ order_by_func = sa.asc > if not query._order_by: E AttributeError: 'Query' object has no attribute '_order_by' ``` ``` tests/functions/test_make_order_by_deterministic.py:74: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ query = <sqlalchemy.orm.query.Query object at 0x7f03d5af64f0> def make_order_by_deterministic(query): else: order_by_func = sa.asc > column = order_by.get_children()[0] E TypeError: 'itertools.chain' object is not subscriptable ``` * Add message to assert * Debug test failures under SQLAlchemy 1.4 * Don't mask potential `AttributeError` exceptions in `sa.engine.URL.create()` * Fix wrong `dialect_name` check Co-authored-by: Nicola Soranzo <[email protected]> * Remove `u` unnecessary on Python 3 * Drop unnecessary `alias()` call This returns a `Subquery` object which is not executable in SQLAlchemy 1.4: ``` tests/relationships/test_select_correlated_expression.py:399: assert session.execute(aggregate) .venv/lib/python3.8/site-packages/sqlalchemy/orm/session.py:1587: in execute statement = coercions.expect(roles.StatementRole, statement) .venv/lib/python3.8/site-packages/sqlalchemy/sql/coercions.py:200: in expect return impl._implicit_coercions( .venv/lib/python3.8/site-packages/sqlalchemy/sql/coercions.py:836: in _implicit_coercions return super(StatementImpl, self)._implicit_coercions( .venv/lib/python3.8/site-packages/sqlalchemy/sql/coercions.py:242: in _implicit_coercions self._raise_for_expected(element, argname, resolved) .venv/lib/python3.8/site-packages/sqlalchemy/sql/coercions.py:270: in _raise_for_expected util.raise_(exc.ArgumentError(msg, code=code), replace_context=err) E sqlalchemy.exc.ArgumentError: Executable SQL or text() construct expected, got <sqlalchemy.sql.selectable.Subquery at 0x7fb34b5e1880; test>. ``` * fix get_columns tests * fix instant defaults listener * remove sort_query and get_query_entities The sort_query and get_query_entities functions never worked fully as intended and contained lots of quirky edge cases: 1. sort_query function was dangerous in a sense that it could be used for really inefficient queries (sorting by non-indexed column). 2. The entity string label introspection in both functions relied on SQLAlchemy internals which were drastically changed in SA 1.4. Relying on those was never a good idea in the first place. * Remove also `get_query_entity_by_alias()` which depends on `get_query_entities()` which was removed in commit 460e1da . * Fix linting errors * fix scalar_subquery warnings * Update sqlalchemy_utils/compat.py Co-authored-by: Nicola Soranzo <[email protected]> * fix selectable columns warning * Add health checks for SQL Server docker service Remove `!` from password so `SA_PASSWORD` doesn't need extra quotes. * Fix install_mssql.sh commands * Use `isolation_level` argument also for `mssql` * Restore `engine.dispose()` * Add `coverage.xml` to `.gitignore` * change tests to use non-deprecated class Change EncryptedType tests to use StringEncryptedType * use scalar subquery for sqlalchemy 1.4 * use create_mock_engine for SA 1.4 * isort linting fixes Co-authored-by: Matthias Bernt <[email protected]> Co-authored-by: Srinivasan R <[email protected]> Co-authored-by: Luca Venturini <[email protected]> Co-authored-by: Braden Mars <[email protected]> Co-authored-by: Konsta Vesterinen <[email protected]>
1 parent 2e8ee00 commit a4154bd

40 files changed

+447
-1092
lines changed

Diff for: .ci/install_mssql.sh

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/sh
2+
3+
if [ ! -f /etc/apt/sources.list.d/microsoft-prod.list ]; then
4+
curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
5+
sudo sh -c "curl https://packages.microsoft.com/config/ubuntu/$(lsb_release -r -s)/prod.list > /etc/apt/sources.list.d/mssql-release.list"
6+
fi
7+
8+
sudo apt-get update
9+
sudo ACCEPT_EULA=Y apt-get -y install msodbcsql17 unixodbc

Diff for: .github/workflows/lint.yaml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Python linting
2+
on: [push, pull_request]
3+
jobs:
4+
test:
5+
name: Test
6+
runs-on: ubuntu-latest
7+
strategy:
8+
matrix:
9+
python-version: ['3.6', '3.9']
10+
steps:
11+
- uses: actions/checkout@v2
12+
- uses: actions/setup-python@v2
13+
with:
14+
python-version: ${{ matrix.python-version }}
15+
- name: Install tox
16+
run: pip install tox
17+
- name: Run linting
18+
run: tox -e lint

Diff for: .github/workflows/test.yaml

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Tests
2+
on: [push, pull_request]
3+
jobs:
4+
test:
5+
runs-on: ubuntu-latest
6+
strategy:
7+
fail-fast: false
8+
matrix:
9+
python-version: ['3.6', '3.7', '3.8', '3.9']
10+
tox_env: ['sqlalchemy14']
11+
include:
12+
- python-version: '3.6'
13+
tox_env: 'sqlalchemy13'
14+
services:
15+
postgres:
16+
image: postgres:latest
17+
env:
18+
POSTGRES_USER: postgres
19+
POSTGRES_PASSWORD: postgres
20+
POSTGRES_DB: sqlalchemy_utils_test
21+
# Set health checks to wait until PostgreSQL has started
22+
options: >-
23+
--health-cmd pg_isready
24+
--health-interval 10s
25+
--health-timeout 5s
26+
--health-retries 5
27+
ports:
28+
- 5432:5432
29+
mysql:
30+
image: mysql:latest
31+
env:
32+
MYSQL_ALLOW_EMPTY_PASSWORD: yes
33+
MYSQL_DATABASE: sqlalchemy_utils_test
34+
ports:
35+
- 3306:3306
36+
mssql:
37+
image: mcr.microsoft.com/mssql/server:2017-latest
38+
env:
39+
ACCEPT_EULA: Y
40+
SA_PASSWORD: Strong_Passw0rd
41+
# Set health checks to wait until SQL Server has started
42+
options: >-
43+
--health-cmd "/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P ${SA_PASSWORD} -Q 'SELECT 1;' -b"
44+
--health-interval 10s
45+
--health-timeout 5s
46+
--health-retries 5
47+
ports:
48+
- 1433:1433
49+
steps:
50+
- uses: actions/checkout@v2
51+
- name: Set up Python
52+
uses: actions/setup-python@v2
53+
with:
54+
python-version: ${{ matrix.python-version }}
55+
- name: Install MS SQL stuff
56+
run: bash .ci/install_mssql.sh
57+
- name: Add hstore extension to the sqlalchemy_utils_test database
58+
env:
59+
PGHOST: localhost
60+
PGPASSWORD: postgres
61+
PGPORT: 5432
62+
run: psql -U postgres -d sqlalchemy_utils_test -c 'CREATE EXTENSION hstore;'
63+
- name: Install tox
64+
run: pip install tox
65+
- name: Run tests
66+
env:
67+
SQLALCHEMY_UTILS_TEST_POSTGRESQL_PASSWORD: postgres
68+
run: tox -e ${{matrix.tox_env }}

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pip-log.txt
2727
# Unit test / coverage reports
2828
.coverage
2929
.tox
30+
coverage.xml
3031
nosetests.xml
3132

3233
# Translations

Diff for: .isort.cfg

-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ known_first_party=sqlalchemy_utils
33
known_third_party=flexmock
44
line_length=79
55
multi_line_output=3
6-
not_skip=__init__.py
76
order_by_type=false

Diff for: .travis.yml

-40
This file was deleted.

Diff for: .travis/install_mssql.sh

-23
This file was deleted.

Diff for: conftest.py

+15-5
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
from sqlalchemy.ext.hybrid import hybrid_property
99
from sqlalchemy.orm import sessionmaker
1010
from sqlalchemy.orm.session import close_all_sessions
11+
1112
from sqlalchemy_utils import (
1213
aggregates,
1314
coercion_listener,
1415
i18n,
1516
InstrumentedList
1617
)
17-
18+
from sqlalchemy_utils.functions.orm import _get_class_registry
1819
from sqlalchemy_utils.types.pg_composite import remove_composite_listeners
1920

2021

@@ -48,14 +49,23 @@ def postgresql_db_user():
4849
return os.environ.get('SQLALCHEMY_UTILS_TEST_POSTGRESQL_USER', 'postgres')
4950

5051

52+
@pytest.fixture(scope='session')
53+
def postgresql_db_password():
54+
return os.environ.get('SQLALCHEMY_UTILS_TEST_POSTGRESQL_PASSWORD', '')
55+
56+
5157
@pytest.fixture(scope='session')
5258
def mysql_db_user():
5359
return os.environ.get('SQLALCHEMY_UTILS_TEST_MYSQL_USER', 'root')
5460

5561

5662
@pytest.fixture
57-
def postgresql_dsn(postgresql_db_user, db_name):
58-
return 'postgresql://{0}@localhost/{1}'.format(postgresql_db_user, db_name)
63+
def postgresql_dsn(postgresql_db_user, postgresql_db_password, db_name):
64+
return 'postgresql://{0}:{1}@localhost/{2}'.format(
65+
postgresql_db_user,
66+
postgresql_db_password,
67+
db_name
68+
)
5969

6070

6171
@pytest.fixture
@@ -86,7 +96,7 @@ def mssql_db_user():
8696
@pytest.fixture
8797
def mssql_db_password():
8898
return os.environ.get('SQLALCHEMY_UTILS_TEST_MSSQL_PASSWORD',
89-
'Strong!Passw0rd')
99+
'Strong_Passw0rd')
90100

91101

92102
@pytest.fixture
@@ -166,7 +176,7 @@ def articles_count(self):
166176

167177
@articles_count.expression
168178
def articles_count(cls):
169-
Article = Base._decl_class_registry['Article']
179+
Article = _get_class_registry(Base)['Article']
170180
return (
171181
sa.select([sa.func.count(Article.id)])
172182
.where(Article.category_id == cls.id)

Diff for: docs/conf.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
# All configuration values have a default; values that are commented out
1212
# serve to show the default.
1313

14-
import sys, os
14+
import os
15+
import sys
1516

1617
# If extensions (or modules to document with autodoc) are in another directory,
1718
# add these directories to sys.path here. If the directory is relative to the

Diff for: setup.cfg

+9
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,11 @@
11
[bdist_wheel]
22
universal = 1
3+
4+
[flake8]
5+
exclude =
6+
.eggs
7+
.git
8+
.tox
9+
.venv
10+
build
11+
docs/conf.py

Diff for: setup.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
55
Various utility functions and custom data types for SQLAlchemy.
66
"""
7-
from setuptools import setup, find_packages
87
import os
98
import re
109
import sys
1110

11+
from setuptools import find_packages, setup
1212

1313
HERE = os.path.dirname(os.path.abspath(__file__))
1414
PY3 = sys.version_info[0] == 3
@@ -58,9 +58,9 @@ def get_version():
5858

5959
# Add all optional dependencies to testing requirements.
6060
test_all = []
61-
for name, requirements in sorted(extras_require.items()):
61+
for requirements in extras_require.values():
6262
test_all += requirements
63-
extras_require['test_all'] = test_all
63+
extras_require['test_all'] = sorted(test_all)
6464

6565

6666
setup(

Diff for: sqlalchemy_utils/__init__.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
get_hybrid_properties,
2626
get_mapper,
2727
get_primary_keys,
28-
get_query_entities,
2928
get_referencing_foreign_keys,
3029
get_tables,
3130
get_type,
@@ -42,7 +41,6 @@
4241
naturally_equivalent,
4342
render_expression,
4443
render_statement,
45-
sort_query,
4644
table_name
4745
)
4846
from .generic import generic_relationship # noqa
@@ -91,6 +89,7 @@
9189
remove_composite_listeners,
9290
ScalarListException,
9391
ScalarListType,
92+
StringEncryptedType,
9493
TimezoneType,
9594
TSVectorType,
9695
URLType,

Diff for: sqlalchemy_utils/aggregates.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ class Rating(Base):
369369
from sqlalchemy.ext.declarative import declared_attr
370370
from sqlalchemy.sql.functions import _FunctionGenerator
371371

372+
from .compat import get_scalar_subquery
372373
from .functions.orm import get_column_key
373374
from .relationships import (
374375
chained_join,
@@ -452,7 +453,7 @@ def aggregate_query(self):
452453
self.relationships[0].mapper.class_
453454
)
454455

455-
return query.as_scalar()
456+
return get_scalar_subquery(query)
456457

457458
def update_query(self, objects):
458459
table = self.class_.__table__

Diff for: sqlalchemy_utils/compat.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def get_scalar_subquery(query):
2+
try:
3+
return query.scalar_subquery()
4+
except AttributeError: # SQLAlchemy <1.4
5+
return query.as_scalar()

Diff for: sqlalchemy_utils/functions/__init__.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
get_hybrid_properties,
2929
get_mapper,
3030
get_primary_keys,
31-
get_query_entities,
3231
get_tables,
3332
get_type,
3433
getdotattr,
@@ -40,8 +39,4 @@
4039
table_name
4140
)
4241
from .render import render_expression, render_statement # noqa
43-
from .sort_query import ( # noqa
44-
make_order_by_deterministic,
45-
QuerySorterException,
46-
sort_query
47-
)
42+
from .sort_query import make_order_by_deterministic # noqa

0 commit comments

Comments
 (0)