Skip to content

Commit 1a74533

Browse files
zzzeekGerrit Code Review
authored andcommitted
Merge "Emit v2.0 deprecation warning for "implicit autocommit""
2 parents 52a80ae + dc91c7d commit 1a74533

File tree

19 files changed

+525
-112
lines changed

19 files changed

+525
-112
lines changed

doc/build/changelog/migration_14.rst

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,124 @@ is established as the implementation.
409409
:ticket:`1390`
410410

411411

412+
.. _deprecation_20_mode:
413+
414+
SQLAlchemy 2.0 Deprecations Mode
415+
---------------------------------
416+
417+
One of the primary goals of the 1.4 release is to provide a "transitional"
418+
release so that applications may migrate to SQLAlchemy 2.0 gradually. Towards
419+
this end, a primary feature in release 1.4 is "2.0 deprecations mode", which is
420+
a series of deprecation warnings that emit against every detectable API pattern
421+
which will work differently in version 2.0. The warnings all make use of the
422+
:class:`_exc.RemovedIn20Warning` class. As these warnings affect foundational
423+
patterns including the :func:`_sql.select` and :class:`_engine.Engine` constructs, even
424+
simple applications can generate a lot of warnings until appropriate API
425+
changes are made. The warning mode is therefore turned off by default until
426+
the developer enables the environment variable ``SQLALCHEMY_WARN_20=1``.
427+
428+
Given the example program below::
429+
430+
from sqlalchemy import column
431+
from sqlalchemy import create_engine
432+
from sqlalchemy import select
433+
from sqlalchemy import table
434+
435+
436+
engine = create_engine("sqlite://")
437+
438+
engine.execute("CREATE TABLE foo (id integer)")
439+
engine.execute("INSERT INTO foo (id) VALUES (1)")
440+
441+
442+
foo = table("foo", column("id"))
443+
result = engine.execute(select([foo.c.id]))
444+
445+
print(result.fetchall())
446+
447+
The above program uses several patterns that many users will already identify
448+
as "legacy", namely the use of the :meth:`_engine.Engine.execute` method
449+
that's part of the :ref:`connectionlesss execution <dbengine_implicit>`
450+
system. When we run the above program against 1.4, it returns a single line::
451+
452+
$ python test3.py
453+
[(1,)]
454+
455+
To enable "2.0 deprecations mode", we enable the ``SQLALCHEMY_WARN_20=1``
456+
variable::
457+
458+
SQLALCHEMY_WARN_20=1 python test3.py
459+
460+
**IMPORTANT** - older versions of Python may not emit deprecation warnings
461+
by default. To guarantee deprecation warnings, use a `warnings filter`_
462+
that ensures warnings are printed::
463+
464+
SQLALCHEMY_WARN_20=1 python -W always::DeprecationWarning test3.py
465+
466+
.. _warnings filter: https://docs.python.org/3/library/warnings.html#the-warnings-filter
467+
468+
With warnings turned on, our program now has a lot to say::
469+
470+
$ SQLALCHEMY_WARN_20=1 python2 -W always::DeprecationWarning test3.py
471+
test3.py:9: RemovedIn20Warning: The Engine.execute() function/method is considered legacy as of the 1.x series of SQLAlchemy and will be removed in 2.0. All statement execution in SQLAlchemy 2.0 is performed by the Connection.execute() method of Connection, or in the ORM by the Session.execute() method of Session. (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
472+
engine.execute("CREATE TABLE foo (id integer)")
473+
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:2856: RemovedIn20Warning: Passing a string to Connection.execute() is deprecated and will be removed in version 2.0. Use the text() construct, or the Connection.exec_driver_sql() method to invoke a driver-level SQL string. (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
474+
return connection.execute(statement, *multiparams, **params)
475+
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:1639: RemovedIn20Warning: The current statement is being autocommitted using implicit autocommit.Implicit autocommit will be removed in SQLAlchemy 2.0. Use the .begin() method of Engine or Connection in order to use an explicit transaction for DML and DDL statements. (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
476+
self._commit_impl(autocommit=True)
477+
test3.py:10: RemovedIn20Warning: The Engine.execute() function/method is considered legacy as of the 1.x series of SQLAlchemy and will be removed in 2.0. All statement execution in SQLAlchemy 2.0 is performed by the Connection.execute() method of Connection, or in the ORM by the Session.execute() method of Session. (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
478+
engine.execute("INSERT INTO foo (id) VALUES (1)")
479+
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:2856: RemovedIn20Warning: Passing a string to Connection.execute() is deprecated and will be removed in version 2.0. Use the text() construct, or the Connection.exec_driver_sql() method to invoke a driver-level SQL string. (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
480+
return connection.execute(statement, *multiparams, **params)
481+
/home/classic/dev/sqlalchemy/lib/sqlalchemy/engine/base.py:1639: RemovedIn20Warning: The current statement is being autocommitted using implicit autocommit.Implicit autocommit will be removed in SQLAlchemy 2.0. Use the .begin() method of Engine or Connection in order to use an explicit transaction for DML and DDL statements. (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
482+
self._commit_impl(autocommit=True)
483+
/home/classic/dev/sqlalchemy/lib/sqlalchemy/sql/selectable.py:4271: RemovedIn20Warning: The legacy calling style of select() is deprecated and will be removed in SQLAlchemy 2.0. Please use the new calling style described at select(). (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
484+
return cls.create_legacy_select(*args, **kw)
485+
test3.py:14: RemovedIn20Warning: The Engine.execute() function/method is considered legacy as of the 1.x series of SQLAlchemy and will be removed in 2.0. All statement execution in SQLAlchemy 2.0 is performed by the Connection.execute() method of Connection, or in the ORM by the Session.execute() method of Session. (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9) (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)
486+
result = engine.execute(select([foo.c.id]))
487+
[(1,)]
488+
489+
With the above guidance, we can migrate our program to use 2.0 styles, and
490+
as a bonus our program is much clearer::
491+
492+
from sqlalchemy import column
493+
from sqlalchemy import create_engine
494+
from sqlalchemy import select
495+
from sqlalchemy import table
496+
from sqlalchemy import text
497+
498+
499+
engine = create_engine("sqlite://")
500+
501+
# don't rely on autocommit for DML and DDL
502+
with engine.begin() as connection:
503+
# use connection.execute(), not engine.execute()
504+
# use the text() construct to execute textual SQL
505+
connection.execute(text("CREATE TABLE foo (id integer)"))
506+
connection.execute(text("INSERT INTO foo (id) VALUES (1)"))
507+
508+
509+
foo = table("foo", column("id"))
510+
511+
with engine.connect() as connection:
512+
# use connection.execute(), not engine.execute()
513+
# select() now accepts column / table expressions positionally
514+
result = connection.execute(select(foo.c.id))
515+
516+
print(result.fetchall())
517+
518+
519+
The goal of "2.0 deprecations mode" is that a program which runs with no
520+
:class:`_exc.RemovedIn20Warning` warnings with "2.0 deprecations mode" turned
521+
on is then ready to run in SQLAlchemy 2.0.
522+
523+
524+
.. seealso::
525+
526+
:ref:`migration_20_toplevel`
527+
528+
529+
412530
API and Behavioral Changes - Core
413531
==================================
414532

doc/build/changelog/migration_20.rst

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,18 @@ The steps to achieve this are as follows:
7777
well as providing for the initial real-world adoption of the new
7878
architectures.
7979

80-
* A new deprecation class :class:`.exc.RemovedIn20Warning` is added, which
81-
subclasses :class:`.exc.SADeprecationWarning`. Applications and their test
80+
* A new deprecation class :class:`_exc.RemovedIn20Warning` is added, which
81+
subclasses :class:`_exc.SADeprecationWarning`. Applications and their test
8282
suites can opt to enable or disable reporting of the
83-
:class:`.exc.RemovedIn20Warning` warning as needed. To some extent, the
84-
:class:`.exc.RemovedIn20Warning` deprecation class is analogous to the ``-3``
83+
:class:`_exc.RemovedIn20Warning` warning as needed, by setting the
84+
environment variable ``SQLALCHEMY_WARN_20=1`` **before** the program
85+
runs. To some extent, the
86+
:class:`_exc.RemovedIn20Warning` deprecation class is analogous to the ``-3``
8587
flag available on Python 2 which reports on future Python 3
86-
incompatibilities.
88+
incompatibilities. See :ref:`deprecation_20_mode` for background
89+
on turning this on.
8790

88-
* APIs which emit :class:`.exc.RemovedIn20Warning` should always feature a new
91+
* APIs which emit :class:`_exc.RemovedIn20Warning` should always feature a new
8992
1.4-compatible usage pattern that applications can migrate towards. This
9093
pattern will then be fully compatible with SQLAlchemy 2.0. In this way,
9194
an application can gradually adjust all of its 1.4-style code to work fully
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.. change::
2+
:tags: engine
3+
:tickets: 4846
4+
5+
"Implicit autocommit", which is the COMMIT that occurs when a DML or DDL
6+
statement is emitted on a connection, is deprecated and won't be part of
7+
SQLAlchemy 2.0. A 2.0-style warning is emitted when autocommit takes
8+
effect, so that the calling code may be adjusted to use an explicit
9+
transaction.
10+
11+
As part of this change, DDL methods such as
12+
:meth:`_schema.MetaData.create_all` when used against an
13+
:class:`_engine.Engine` will run the operation in a BEGIN block if one is
14+
not started already.
15+
16+
.. seealso::
17+
18+
:ref:`deprecation_20_mode`
19+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.. change::
2+
:tags: bug, mysql
3+
4+
The MySQL and MariaDB dialects now query from the information_schema.tables
5+
system view in order to determine if a particular table exists or not.
6+
Previously, the "DESCRIBE" command was used with an exception catch to
7+
detect non-existent, which would have the undesirable effect of emitting a
8+
ROLLBACK on the connection. There appeared to be legacy encoding issues
9+
which prevented the use of "SHOW TABLES", for this, but as MySQL support is
10+
now at 5.0.2 or above due to :ticket:`4189`, the information_schema tables
11+
are now available in all cases.
12+

doc/build/core/tutorial.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,22 +150,26 @@ each table first before creating, so it's safe to call multiple times:
150150
.. sourcecode:: pycon+sql
151151

152152
{sql}>>> metadata.create_all(engine)
153-
PRAGMA...
153+
BEGIN...
154154
CREATE TABLE users (
155155
id INTEGER NOT NULL,
156156
name VARCHAR,
157157
fullname VARCHAR,
158158
PRIMARY KEY (id)
159159
)
160+
<BLANKLINE>
161+
<BLANKLINE>
160162
[...] ()
161-
COMMIT
163+
<BLANKLINE>
162164
CREATE TABLE addresses (
163165
id INTEGER NOT NULL,
164166
user_id INTEGER,
165167
email_address VARCHAR NOT NULL,
166168
PRIMARY KEY (id),
167169
FOREIGN KEY(user_id) REFERENCES users (id)
168170
)
171+
<BLANKLINE>
172+
<BLANKLINE>
169173
[...] ()
170174
COMMIT
171175

doc/build/errors.rst

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,20 +104,19 @@ a comprehensive future compatibility system that is to be integrated into the
104104
unambiguous, and incremental upgrade path in order to migrate applications to
105105
being fully 2.0 compatible. The :class:`.exc.RemovedIn20Warning` deprecation
106106
warning is at the base of this system to provide guidance on what behaviors in
107-
an existing codebase will need to be modified.
108-
109-
For some occurrences of this warning, an additional recommendation to use an
110-
API in either the ``sqlalchemy.future`` or ``sqlalchemy.future.orm`` packages
111-
may be present. This refers to two special future-compatibility packages that
112-
are part of SQLAlchemy 1.4 and are there to help migrate an application to the
113-
2.0 version.
107+
an existing codebase will need to be modified. An overview of how to enable
108+
this warning is at :ref:`deprecation_20_mode`.
114109

115110
.. seealso::
116111

117112
:ref:`migration_20_toplevel` - An overview of the upgrade process from
118113
the 1.x series, as well as the current goals and progress of SQLAlchemy
119114
2.0.
120115

116+
117+
:ref:`deprecation_20_mode` - specific guidelines on how to use
118+
"2.0 deprecations mode" in SQLAlchemy 1.4.
119+
121120
.. _error_c9bf:
122121

123122
A bind was located via legacy bound metadata, but since future=True is set on this Session, this bind is ignored.

doc/build/orm/tutorial.rst

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,16 +209,16 @@ the actual ``CREATE TABLE`` statement:
209209
.. sourcecode:: python+sql
210210

211211
>>> Base.metadata.create_all(engine)
212-
PRAGMA main.table_info("users")
213-
[...] ()
214-
PRAGMA temp.table_info("users")
215-
[...] ()
212+
BEGIN...
216213
CREATE TABLE users (
217-
id INTEGER NOT NULL, name VARCHAR,
214+
id INTEGER NOT NULL,
215+
name VARCHAR,
218216
fullname VARCHAR,
219217
nickname VARCHAR,
220218
PRIMARY KEY (id)
221219
)
220+
<BLANKLINE>
221+
<BLANKLINE>
222222
[...] ()
223223
COMMIT
224224

@@ -1215,14 +1215,16 @@ already been created:
12151215
.. sourcecode:: python+sql
12161216

12171217
{sql}>>> Base.metadata.create_all(engine)
1218-
PRAGMA...
1218+
BEGIN...
12191219
CREATE TABLE addresses (
12201220
id INTEGER NOT NULL,
12211221
email_address VARCHAR NOT NULL,
12221222
user_id INTEGER,
12231223
PRIMARY KEY (id),
1224-
FOREIGN KEY(user_id) REFERENCES users (id)
1224+
FOREIGN KEY(user_id) REFERENCES users (id)
12251225
)
1226+
<BLANKLINE>
1227+
<BLANKLINE>
12261228
[...] ()
12271229
COMMIT
12281230

@@ -2080,15 +2082,17 @@ Create new tables:
20802082
.. sourcecode:: python+sql
20812083

20822084
{sql}>>> Base.metadata.create_all(engine)
2083-
PRAGMA...
2085+
BEGIN...
20842086
CREATE TABLE keywords (
20852087
id INTEGER NOT NULL,
20862088
keyword VARCHAR(50) NOT NULL,
20872089
PRIMARY KEY (id),
20882090
UNIQUE (keyword)
20892091
)
2092+
<BLANKLINE>
2093+
<BLANKLINE>
20902094
[...] ()
2091-
COMMIT
2095+
<BLANKLINE>
20922096
CREATE TABLE posts (
20932097
id INTEGER NOT NULL,
20942098
user_id INTEGER,
@@ -2097,15 +2101,19 @@ Create new tables:
20972101
PRIMARY KEY (id),
20982102
FOREIGN KEY(user_id) REFERENCES users (id)
20992103
)
2104+
<BLANKLINE>
2105+
<BLANKLINE>
21002106
[...] ()
2101-
COMMIT
2107+
<BLANKLINE>
21022108
CREATE TABLE post_keywords (
21032109
post_id INTEGER NOT NULL,
21042110
keyword_id INTEGER NOT NULL,
21052111
PRIMARY KEY (post_id, keyword_id),
21062112
FOREIGN KEY(post_id) REFERENCES posts (id),
21072113
FOREIGN KEY(keyword_id) REFERENCES keywords (id)
21082114
)
2115+
<BLANKLINE>
2116+
<BLANKLINE>
21092117
[...] ()
21102118
COMMIT
21112119

lib/sqlalchemy/dialects/mysql/base.py

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ class MyClass(Base):
887887
import re
888888

889889
from sqlalchemy import literal_column
890+
from sqlalchemy import text
890891
from sqlalchemy.sql import visitors
891892
from . import reflection as _reflection
892893
from .enumerated import ENUM
@@ -938,6 +939,7 @@ class MyClass(Base):
938939
from ...sql import elements
939940
from ...sql import roles
940941
from ...sql import util as sql_util
942+
from ...sql.sqltypes import Unicode
941943
from ...types import BINARY
942944
from ...types import BLOB
943945
from ...types import BOOLEAN
@@ -2708,39 +2710,24 @@ def _get_default_schema_name(self, connection):
27082710
return connection.exec_driver_sql("SELECT DATABASE()").scalar()
27092711

27102712
def has_table(self, connection, table_name, schema=None):
2711-
# SHOW TABLE STATUS LIKE and SHOW TABLES LIKE do not function properly
2712-
# on macosx (and maybe win?) with multibyte table names.
2713-
#
2714-
# TODO: if this is not a problem on win, make the strategy swappable
2715-
# based on platform. DESCRIBE is slower.
2716-
2717-
# [ticket:726]
2718-
# full_name = self.identifier_preparer.format_table(table,
2719-
# use_schema=True)
2713+
if schema is None:
2714+
schema = self.default_schema_name
27202715

2721-
full_name = ".".join(
2722-
self.identifier_preparer._quote_free_identifiers(
2723-
schema, table_name
2724-
)
2716+
rs = connection.execute(
2717+
text(
2718+
"SELECT * FROM information_schema.tables WHERE "
2719+
"table_schema = :table_schema AND "
2720+
"table_name = :table_name"
2721+
).bindparams(
2722+
sql.bindparam("table_schema", type_=Unicode),
2723+
sql.bindparam("table_name", type_=Unicode),
2724+
),
2725+
{
2726+
"table_schema": util.text_type(schema),
2727+
"table_name": util.text_type(table_name),
2728+
},
27252729
)
2726-
2727-
st = "DESCRIBE %s" % full_name
2728-
rs = None
2729-
try:
2730-
try:
2731-
rs = connection.execution_options(
2732-
skip_user_error_events=True
2733-
).exec_driver_sql(st)
2734-
have = rs.fetchone() is not None
2735-
rs.close()
2736-
return have
2737-
except exc.DBAPIError as e:
2738-
if self._extract_error_code(e.orig) == 1146:
2739-
return False
2740-
raise
2741-
finally:
2742-
if rs:
2743-
rs.close()
2730+
return bool(rs.scalar())
27442731

27452732
def has_sequence(self, connection, sequence_name, schema=None):
27462733
if not self.supports_sequences:

0 commit comments

Comments
 (0)