Convert remaining ORM APIs to support 2.0 style

This is kind of a mixed bag of all kinds to help get us
to 1.4 betas.    The documentation stuff is a work in
progress.    Lots of other relatively small changes to
APIs and things.    More commits will follow to continue
improving the documentation and transitioning to the
1.4/2.0 hybrid documentation.  In particular some refinements
to Session usage models so that it can match Engine's
scoping / transactional patterns, and a decision to
start moving away from "subtransactions" completely.

* add select().from_statement() to produce FromStatement in an
  ORM context

* begin referring to select() that has "plugins" for the few edge
  cases where select() will have ORM-only behaviors

* convert dynamic.AppenderQuery to its own object that can use
  select(), though at the moment it uses Query to support legacy
  join calling forms.

* custom query classes for AppenderQuery are replaced by
  do_orm_execute() hooks for custom actions, a separate gerrit
  will document this

* add Session.get() to replace query.get()

* Deprecate session.begin->subtransaction.  propose within the
  test suite a hypothetical recipe for apps that rely on this
  pattern

* introduce Session construction level context manager,
  sessionmaker context manager, rewrite the whole top of the
  session_transaction.rst documentation.   Establish context manager
  patterns for Session that are identical to engine

* ensure same begin_nested() / commit() behavior as engine

* devise all new "join into an external transaction" recipe,
  add test support for it, add rules into Session so it
  just works, write new docs.  need to ensure this doesn't
  break anything

* vastly reduce the verbosity of lots of session docs as
  I dont think people read this stuff and it's difficult
  to keep current in any case

* constructs like case(), with_only_columns() really need
  to move to *columns, add a coercion rule to just change
  these.

* docs need changes everywhere I look.  in_() is not in
  the Core tutorial?  how do people even know about it?
  Remove tons of cruft from Select docs, etc.

* build a system for common ORM options like populate_existing
  and autoflush to populate from execution options.

* others?

Change-Id: Ia4bea0f804250e54d90b3884cf8aab8b66b82ecf
This commit is contained in:
Mike Bayer
2020-07-08 14:31:17 -04:00
parent e2d4b2e72c
commit 5de0f1cf50
45 changed files with 4200 additions and 2045 deletions
+1
View File
@@ -738,6 +738,7 @@ In 2.0, an application that still wishes to use a separate :class:`_schema.Table
does not want to use Declarative with ``__table__``, can instead use the above
pattern which basically does the same thing.
.. _migration_20_unify_select:
ORM Query Unified with Core Select
==================================
+23
View File
@@ -3,6 +3,29 @@
SQLAlchemy 2.0 Future (Core)
============================
This package includes a relatively small number of transitional elements
to allow "2.0 mode" to take place within SQLAlchemy 1.4. The primary
objects provided here are :class:`_future.Engine` and :class:`_future.Connection`,
which are both subclasses of the existing :class:`_engine.Engine` and
:class:`_engine.Connection` objects with essentially a smaller set of
methods and the removal of "autocommit".
Within the 1.4 series, the "2.0" style of engines and connections is enabled
by passing the :paramref:`_sa.create_engine.future` flag to
:func:`_sa.create_engine`::
from sqlalchemy import create_engine
engine = create_engine("postgresql://user:pass@host/dbname", future=True)
Similarly, with the ORM, to enable "future" behavior in the ORM :class:`.Session`,
pass the :paramref:`_orm.Session.future` parameter either to the
:class:`.Session` constructor directly, or via the :class:`_orm.sessionmaker`
class::
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(engine, future=True)
.. seealso::
:ref:`migration_20_toplevel` - Introduction to the 2.0 series of SQLAlchemy
+7 -1
View File
@@ -80,16 +80,22 @@ elements are themselves :class:`_expression.ColumnElement` subclasses).
.. autoclass:: Lateral
:members:
.. autoclass:: ReturnsRows
:members:
:inherited-members: ClauseElement
.. autoclass:: ScalarSelect
:members:
.. autoclass:: Select
:members:
:inherited-members: ClauseElement
:exclude-members: memoized_attribute, memoized_instancemethod
:exclude-members: memoized_attribute, memoized_instancemethod, append_correlation, append_column, append_prefix, append_whereclause, append_having, append_from, append_order_by, append_group_by
.. autoclass:: Selectable
:members:
:inherited-members: ClauseElement
.. autoclass:: SelectBase
:members:
+106 -1
View File
@@ -716,7 +716,112 @@ will ensure that the return type of the expression is handled as boolean::
somecolumn.bool_op('-->')('some value')
.. versionadded:: 1.2.0b3 Added the :meth:`.Operators.bool_op` method.
Commonly Used Operators
-------------------------
Here's a rundown of some of the most common operators used in both the
Core expression language as well as in the ORM. Here we see expressions
that are most commonly present when using the :meth:`_sql.Select.where` method,
but can be used in other scenarios as well.
A listing of all the column-level operations common to all column-like
objects is at :class:`.ColumnOperators`.
* :meth:`equals <.ColumnOperators.__eq__>`::
statement.where(users.c.name == 'ed')
* :meth:`not equals <.ColumnOperators.__ne__>`::
statement.where(users.c.name != 'ed')
* :meth:`LIKE <.ColumnOperators.like>`::
statement.where(users.c.name.like('%ed%'))
.. note:: :meth:`.ColumnOperators.like` renders the LIKE operator, which
is case insensitive on some backends, and case sensitive
on others. For guaranteed case-insensitive comparisons, use
:meth:`.ColumnOperators.ilike`.
* :meth:`ILIKE <.ColumnOperators.ilike>` (case-insensitive LIKE)::
statement.where(users.c.name.ilike('%ed%'))
.. note:: most backends don't support ILIKE directly. For those,
the :meth:`.ColumnOperators.ilike` operator renders an expression
combining LIKE with the LOWER SQL function applied to each operand.
* :meth:`IN <.ColumnOperators.in_>`::
statement.where(users.c..name.in_(['ed', 'wendy', 'jack']))
# works with Select objects too:
statement.where.filter(users.c.name.in_(
select(users.c.name).where(users.c.name.like('%ed%'))
))
# use tuple_() for composite (multi-column) queries
from sqlalchemy import tuple_
statement.where(
tuple_(users.c.name, users.c.nickname).\
in_([('ed', 'edsnickname'), ('wendy', 'windy')])
)
* :meth:`NOT IN <.ColumnOperators.notin_>`::
statement.where(~users.c.name.in_(['ed', 'wendy', 'jack']))
* :meth:`IS NULL <.ColumnOperators.is_>`::
statement.where(users.c. == None)
# alternatively, if pep8/linters are a concern
statement.where(users.c.name.is_(None))
* :meth:`IS NOT NULL <.ColumnOperators.isnot>`::
statement.where(users.c.name != None)
# alternatively, if pep8/linters are a concern
statement.where(users.c.name.isnot(None))
* :func:`AND <.sql.expression.and_>`::
# use and_()
from sqlalchemy import and_
statement.where(and_(users.c.name == 'ed', users.c.fullname == 'Ed Jones'))
# or send multiple expressions to .where()
statement.where(users.c.name == 'ed', users.c.fullname == 'Ed Jones')
# or chain multiple where() calls
statement.where(users.c.name == 'ed').where(users.c.fullname == 'Ed Jones')
.. note:: Make sure you use :func:`.and_` and **not** the
Python ``and`` operator!
* :func:`OR <.sql.expression.or_>`::
from sqlalchemy import or_
statement.where(or_(users.c.name == 'ed', users.c.name == 'wendy'))
.. note:: Make sure you use :func:`.or_` and **not** the
Python ``or`` operator!
* :meth:`MATCH <.ColumnOperators.match>`::
statement.where(users.c.name.match('wendy'))
.. note::
:meth:`~.ColumnOperators.match` uses a database-specific ``MATCH``
or ``CONTAINS`` function; its behavior will vary by backend and is not
available on some backends such as SQLite.
Operator Customization
----------------------
+35
View File
@@ -9,6 +9,22 @@ Glossary
.. glossary::
:sorted:
1.x style
2.0 style
1.x-style
2.0-style
These terms are new in SQLAlchemy 1.4 and refer to the SQLAlchemy 1.4->
2.0 transition plan, described at :ref:`migration_20_toplevel`. The
term "1.x style" refers to an API used in the way it's been documented
throughout the 1.x series of SQLAlhcemy and earlier (e.g. 1.3, 1.2, etc)
and the term "2.0 style" refers to the way an API will look in version
2.0. Version 1.4 implements nearly all of 2.0's API in so-called
"transition mode".
.. seealso::
:ref:`migration_20_toplevel`
relational
relational algebra
@@ -49,6 +65,25 @@ Glossary
in terms of one particular table alias or another, based on its position
within the join expression.
plugin
plugin-specific
"plugin-specific" generally indicates a function or method in
SQLAlchemy Core which will behave differently when used in an ORM
context.
SQLAlchemy allows Core consrtucts such as :class:`_sql.Select` objects
to participate in a "plugin" system, which can inject additional
behaviors and features into the object that are not present by default.
Specifically, the primary "plugin" is the "orm" plugin, which is
at the base of the system that the SQLAlchemy ORM makes use of
Core constructs in order to compose and execute SQL queries that
return ORM results.
.. seealso::
:ref:`migration_20_unify_select`
crud
CRUD
An acronym meaning "Create, Update, Delete". The term in SQL refers to the
+1 -1
View File
@@ -91,7 +91,7 @@ are documented here. In contrast to the ORM's domain-centric mode of usage, the
:doc:`Core Event Interfaces <core/events>` |
:doc:`Creating Custom SQL Constructs <core/compiler>` |
* **SQLAlchemy 2.0 Compatibility:** :doc:`SQLAlchemy 2.0 Future (Core) <core/future>`
* **SQLAlchemy 2.0 Compatibility:** :ref:`migration_20_toplevel`
Dialect Documentation
======================
+10 -6
View File
@@ -32,11 +32,12 @@ loading of child items both at load time as well as deletion time.
Dynamic Relationship Loaders
----------------------------
A key feature to enable management of a large collection is the so-called "dynamic"
relationship. This is an optional form of :func:`~sqlalchemy.orm.relationship` which
returns a :class:`~sqlalchemy.orm.query.Query` object in place of a collection
when accessed. :func:`~sqlalchemy.orm.query.Query.filter` criterion may be
applied as well as limits and offsets, either explicitly or via array slices::
A key feature to enable management of a large collection is the so-called
"dynamic" relationship. This is an optional form of
:func:`_orm.relationship` which returns a
:class:`_orm.AppenderQuery` object in place of a collection
when accessed. Filtering criterion may be applied as well as limits and
offsets, either explicitly or via array slices::
class User(Base):
__tablename__ = 'user'
@@ -52,7 +53,7 @@ applied as well as limits and offsets, either explicitly or via array slices::
posts = jack.posts[5:20]
The dynamic relationship supports limited write operations, via the
``append()`` and ``remove()`` methods::
:meth:`_orm.AppenderQuery.append` and :meth:`_orm.AppenderQuery.remove` methods::
oldpost = jack.posts.filter(Post.headline=='old post').one()
jack.posts.remove(oldpost)
@@ -78,6 +79,9 @@ function in conjunction with ``lazy='dynamic'``::
Note that eager/lazy loading options cannot be used in conjunction dynamic relationships at this time.
.. autoclass:: sqlalchemy.orm.AppenderQuery
:members:
.. note::
The :func:`_orm.dynamic_loader` function is essentially the same
+101
View File
@@ -2,6 +2,107 @@
Additional Persistence Techniques
=================================
.. _session_deleting_from_collections:
Notes on Delete - Deleting Objects Referenced from Collections and Scalar Relationships
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ORM in general never modifies the contents of a collection or scalar
relationship during the flush process. This means, if your class has a
:func:`_orm.relationship` that refers to a collection of objects, or a reference
to a single object such as many-to-one, the contents of this attribute will
not be modified when the flush process occurs. Instead, it is expected
that the :class:`.Session` would eventually be expired, either through the expire-on-commit behavior of
:meth:`.Session.commit` or through explicit use of :meth:`.Session.expire`.
At that point, any referenced object or collection associated with that
:class:`.Session` will be cleared and will re-load itself upon next access.
A common confusion that arises regarding this behavior involves the use of the
:meth:`~.Session.delete` method. When :meth:`.Session.delete` is invoked upon
an object and the :class:`.Session` is flushed, the row is deleted from the
database. Rows that refer to the target row via foreign key, assuming they
are tracked using a :func:`_orm.relationship` between the two mapped object types,
will also see their foreign key attributes UPDATED to null, or if delete
cascade is set up, the related rows will be deleted as well. However, even
though rows related to the deleted object might be themselves modified as well,
**no changes occur to relationship-bound collections or object references on
the objects** involved in the operation within the scope of the flush
itself. This means if the object was a
member of a related collection, it will still be present on the Python side
until that collection is expired. Similarly, if the object were
referenced via many-to-one or one-to-one from another object, that reference
will remain present on that object until the object is expired as well.
Below, we illustrate that after an ``Address`` object is marked
for deletion, it's still present in the collection associated with the
parent ``User``, even after a flush::
>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True
When the above session is committed, all attributes are expired. The next
access of ``user.addresses`` will re-load the collection, revealing the
desired state::
>>> session.commit()
>>> address in user.addresses
False
There is a recipe for intercepting :meth:`.Session.delete` and invoking this
expiration automatically; see `ExpireRelationshipOnFKChange <http://www.sqlalchemy.org/trac/wiki/UsageRecipes/ExpireRelationshipOnFKChange>`_ for this. However, the usual practice of
deleting items within collections is to forego the usage of
:meth:`~.Session.delete` directly, and instead use cascade behavior to
automatically invoke the deletion as a result of removing the object from the
parent collection. The ``delete-orphan`` cascade accomplishes this, as
illustrated in the example below::
class User(Base):
__tablename__ = 'user'
# ...
addresses = relationship(
"Address", cascade="all, delete-orphan")
# ...
del user.addresses[1]
session.flush()
Where above, upon removing the ``Address`` object from the ``User.addresses``
collection, the ``delete-orphan`` cascade has the effect of marking the ``Address``
object for deletion in the same way as passing it to :meth:`~.Session.delete`.
The ``delete-orphan`` cascade can also be applied to a many-to-one
or one-to-one relationship, so that when an object is de-associated from its
parent, it is also automatically marked for deletion. Using ``delete-orphan``
cascade on a many-to-one or one-to-one requires an additional flag
:paramref:`_orm.relationship.single_parent` which invokes an assertion
that this related object is not to shared with any other parent simultaneously::
class User(Base):
# ...
preference = relationship(
"Preference", cascade="all, delete-orphan",
single_parent=True)
Above, if a hypothetical ``Preference`` object is removed from a ``User``,
it will be deleted on flush::
some_user.preference = None
session.flush() # will delete the Preference object
.. seealso::
:ref:`unitofwork_cascades` for detail on cascades.
.. _flush_embedded_sql_expressions:
Embedding SQL Insert/Update Expressions into a Flush
+4 -3
View File
@@ -6,9 +6,10 @@ Using the Session
.. module:: sqlalchemy.orm.session
The :func:`_orm.mapper` function and :mod:`~sqlalchemy.ext.declarative` extensions
are the primary configurational interface for the ORM. Once mappings are
configured, the primary usage interface for persistence operations is the
The declarative base and ORM mapping functions described at
:ref:`mapper_config_toplevel` are the primary configurational interface for the
ORM. Once mappings are configured, the primary usage interface for
persistence operations is the
:class:`.Session`.
.. toctree::
+574 -561
View File
File diff suppressed because it is too large Load Diff
+417 -238
View File
@@ -7,83 +7,119 @@ Transactions and Connection Management
Managing Transactions
=====================
A newly constructed :class:`.Session` may be said to be in the "begin" state.
In this state, the :class:`.Session` has not established any connection or
transactional state with any of the :class:`_engine.Engine` objects that may be associated
with it.
.. versionchanged:: 1.4 Session transaction management has been revised
to be clearer and easier to use. In particular, it now features
"autobegin" operation, which means the point at which a transaction begins
may be controlled, without using the legacy "autocommit" mode.
The :class:`.Session` then receives requests to operate upon a database connection.
Typically, this means it is called upon to execute SQL statements using a particular
:class:`_engine.Engine`, which may be via :meth:`.Session.query`, :meth:`.Session.execute`,
or within a flush operation of pending data, which occurs when such state exists
and :meth:`.Session.commit` or :meth:`.Session.flush` is called.
The :class:`_orm.Session` tracks the state of a single "virtual" transaction
at a time, using an object called
:class:`_orm.SessionTransaction`. This object then makes use of the underyling
:class:`_engine.Engine` or engines to which the :class:`_orm.Session`
object is bound in order to start real connection-level transactions using
the :class:`_engine.Connection` object as needed.
As these requests are received, each new :class:`_engine.Engine` encountered is associated
with an ongoing transactional state maintained by the :class:`.Session`.
When the first :class:`_engine.Engine` is operated upon, the :class:`.Session` can be said
to have left the "begin" state and entered "transactional" state. For each
:class:`_engine.Engine` encountered, a :class:`_engine.Connection` is associated with it,
which is acquired via the :meth:`_engine.Engine.connect` method. If a
:class:`_engine.Connection` was directly associated with the :class:`.Session` (see :ref:`session_external_transaction`
for an example of this), it is
added to the transactional state directly.
This "virtual" transaction is created automatically when needed, or can
alternatively be started using the :meth:`_orm.Session.begin` method. To
as great a degree as possible, Python context manager use is supported both
at the level of creating :class:`_orm.Session` objects as well as to maintain
the scope of the :class:`_orm.SessionTransaction`.
For each :class:`_engine.Connection`, the :class:`.Session` also maintains a :class:`.Transaction` object,
which is acquired by calling :meth:`_engine.Connection.begin` on each :class:`_engine.Connection`,
or if the :class:`.Session`
object has been established using the flag ``twophase=True``, a :class:`.TwoPhaseTransaction`
object acquired via :meth:`_engine.Connection.begin_twophase`. These transactions are all committed or
rolled back corresponding to the invocation of the
:meth:`.Session.commit` and :meth:`.Session.rollback` methods. A commit operation will
also call the :meth:`.TwoPhaseTransaction.prepare` method on all transactions if applicable.
Below, assume we start with a :class:`_orm.Session`::
When the transactional state is completed after a rollback or commit, the :class:`.Session`
:term:`releases` all :class:`.Transaction` and :class:`_engine.Connection` resources,
and goes back to the "begin" state, which
will again invoke new :class:`_engine.Connection` and :class:`.Transaction` objects as new
requests to emit SQL statements are received.
from sqlalchemy.orm import Session
session = Session(engine)
The example below illustrates this lifecycle::
We can now run operations within a demarcated transaction using a context
manager::
engine = create_engine("...")
Session = sessionmaker(bind=engine)
with session.begin():
session.add(some_object())
session.add(some_other_object())
# commits transaction at the end, or rolls back if there
# was an exception raised
# new session. no connections are in use.
session = Session()
try:
# first query. a Connection is acquired
# from the Engine, and a Transaction
# started.
item1 = session.query(Item).get(1)
At the end of the above context, assuming no exceptions were raised, any
pending objects will be flushed to the database and the database transaction
will be committed. If an exception was raised within the above block, then the
transaction would be rolled back. In both cases, the above
:class:`_orm.Session` subsequent to exiting the block is ready to be used in
subsequent transactions.
# second query. the same Connection/Transaction
# are used.
item2 = session.query(Item).get(2)
The :meth:`_orm.Session.begin` method is optional, and the
:class:`_orm.Session` may also be used in a commit-as-you-go approach, where it
will begin transactions automatically as needed; these only need be committed
or rolled back::
# pending changes are created.
item1.foo = 'bar'
item2.bar = 'foo'
session = Session(engine)
# commit. The pending changes above
# are flushed via flush(), the Transaction
# is committed, the Connection object closed
# and discarded, the underlying DBAPI connection
# returned to the connection pool.
session.commit()
except:
# on rollback, the same closure of state
# as that of commit proceeds.
session.rollback()
raise
finally:
# close the Session. This will expunge any remaining
# objects as well as reset any existing SessionTransaction
# state. Neither of these steps are usually essential.
# However, if the commit() or rollback() itself experienced
# an unanticipated internal failure (such as due to a mis-behaved
# user-defined event handler), .close() will ensure that
# invalid state is removed.
session.close()
session.add(some_object())
session.add(some_other_object())
session.commit() # commits
# will automatically begin again
result = session.execute(< some select statment >)
session.add_all([more_objects, ...])
session.commit() # commits
session.add(still_another_object)
session.flush() # flush still_another_object
session.rollback() # rolls back still_another_object
The :class:`_orm.Session` itself features a :meth:`_orm.Session.close`
method. If the :class:`_orm.Session` is begun within a transaction that
has not yet been committed or rolled back, this method will cancel
(i.e. rollback) that transaction, and also expunge all objects contained
within the :class:`_orm.Session` object's state. If the :class:`_orm.Session`
is being used in such a way that a call to :meth:`_orm.Session.commit`
or :meth:`_orm.Session.rollback` is not guaranteed (e.g. not within a context
manager or similar), the :class:`_orm.Session.close` method may be used
to ensure all resources are released::
# expunges all objects, releases all transactions unconditionally
# (with rollback), releases all database connections back to their
# engines
session.close()
Finally, the session construction / close process can itself be run
via context manager. This is the best way to ensure that the scope of
a :class:`_orm.Session` object's use is scoped within a fixed block.
Illustrated via the :class:`_orm.Session` constructor
first::
with Session(engine) as session:
session.add(some_object())
session.add(some_other_object())
session.commit() # commits
session.add(still_another_object)
session.flush() # flush still_another_object
session.commit() # commits
result = session.execute(<some SELECT statement>)
# remaining transactional state from the .execute() call is
# discarded
Similarly, the :class:`_orm.sessionmaker` can be used in the same way::
Session = sesssionmaker(engine)
with Session() as session:
with session.begin():
session.add(some_object)
# commits
# closes the Session
:class:`_orm.sessionmaker` itself includes a :meth:`_orm.sessionmaker.begin`
method to allow both operations to take place at once::
with Session.begin() as session:
session.add(some_object):
@@ -96,39 +132,28 @@ SAVEPOINT transactions, if supported by the underlying engine, may be
delineated using the :meth:`~.Session.begin_nested`
method::
Session = sessionmaker()
session = Session()
session.add(u1)
session.add(u2)
session.begin_nested() # establish a savepoint
session.add(u3)
session.rollback() # rolls back u3, keeps u1 and u2
with Session.begin() as session:
session.add(u1)
session.add(u2)
session.commit() # commits u1 and u2
nested = session.begin_nested() # establish a savepoint
session.add(u3)
nested.rollback() # rolls back u3, keeps u1 and u2
:meth:`~.Session.begin_nested` may be called any number
of times, which will issue a new SAVEPOINT with a unique identifier for each
call. For each :meth:`~.Session.begin_nested` call, a
corresponding :meth:`~.Session.rollback` or
:meth:`~.Session.commit` must be issued. (But note that if the return value is
used as a context manager, i.e. in a with-statement, then this rollback/commit
is issued by the context manager upon exiting the context, and so should not be
added explicitly.)
# commits u1 and u2
When :meth:`~.Session.begin_nested` is called, a
:meth:`~.Session.flush` is unconditionally issued
(regardless of the ``autoflush`` setting). This is so that when a
:meth:`~.Session.rollback` occurs, the full state of the
session is expired, thus causing all subsequent attribute/instance access to
reference the full state of the :class:`~sqlalchemy.orm.session.Session` right
before :meth:`~.Session.begin_nested` was called.
Each time :meth:`_orm.Session.begin_nested` is called, a new "BEGIN SAVEPOINT"
command is emitted to the database wih a unique identifier. When
:meth:`_orm.SessionTransaction.commit` is called, "RELEASE SAVEPOINT"
is emitted on the database, and if instead
:meth:`_orm.SessionTransaction.rollback` is called, "ROLLBACK TO SAVEPOINT"
is emitted.
:meth:`~.Session.begin_nested`, in the same manner as the less often
used :meth:`~.Session.begin` method, returns a :class:`.SessionTransaction` object
which works as a context manager.
It can be succinctly used around individual record inserts in order to catch
things like unique constraint exceptions::
:meth:`_orm.Session.begin_nested` may also be used as a context manager in the
same manner as that of the :meth:`_orm.Session.begin` method::
for record in records:
try:
@@ -138,54 +163,187 @@ things like unique constraint exceptions::
print("Skipped record %s" % record)
session.commit()
When :meth:`~.Session.begin_nested` is called, a
:meth:`~.Session.flush` is unconditionally issued
(regardless of the ``autoflush`` setting). This is so that when a
rollback on this nested transaction occurs, the full state of the
session is expired, thus causing all subsequent attribute/instance access to
reference the full state of the :class:`~sqlalchemy.orm.session.Session` right
before :meth:`~.Session.begin_nested` was called.
Session-level vs. Engine level transaction control
--------------------------------------------------
As of SQLAlchemy 1.4, the :class:`_orm.sessionmaker` and Core
:class:`_engine.Engine` objects both support :term:`2.0 style` operation,
by making use of the :paramref:`_orm.Session.future` flag as well as the
:paramref:`_engine.create_engine.future` flag so that these two objects
assume 2.0-style semantics.
When using future mode, there should be equivalent semantics between
the two packages, at the level of the :class:`_orm.sessionmaker` vs.
the :class:`_future.Engine`, as well as the :class:`_orm.Session` vs.
the :class:`_future.Connection`. The following sections detail
these scenarios based on the following scheme::
ORM (using future Session) Core (using future engine)
----------------------------------------- -----------------------------------
sessionmaker Engine
Session Connection
sessionmaker.begin() Engine.begin()
some_session.commit() some_connection.commit()
with some_sessionmaker() as session: with some_engine.connect() as conn:
with some_sessionmaker.begin() as session: with some_engine.begin() as conn:
with some_session.begin_nested() as sp: with some_connection.begin_nested() as sp:
Commit as you go
~~~~~~~~~~~~~~~~
Both :class:`_orm.Session` and :class:`_future.Connection` feature
:meth:`_future.Connection.commit` and :meth:`_future.Connection.rollback`
methods. Using SQLAlchemy 2.0-style operation, these methods affect the
**outermost** transaction in all cases.
Engine::
engine = create_engine("postgresql://user:pass@host/dbname", future=True)
with engine.connect() as conn:
conn.execute(
some_table.insert(),
[
{"data": "some data one"},
{"data": "some data two"},
{"data": "some data three"}
]
)
conn.commit()
Session::
Session = sessionmaker(engine, future=True)
with Session() as session:
session.add_all([
SomeClass(data="some data one"),
SomeClass(data="some data two"),
SomeClass(data="some data three")
])
session.commit()
Commit at once
~~~~~~~~~~~~~~~~
Both :class:`_orm.sessionmaker` and :class:`_future.Engine` feature a
:meth:`_future.Engine.begin` method that will both procure a new object
with which to execute SQL statements (the :class:`_orm.Session` and
:class:`_future.Connection`, respectively) and then return a context manager
that will maintain a begin/commit/rollback context for that object.
Engine::
engine = create_engine("postgresql://user:pass@host/dbname", future=True)
with engine.begin() as conn:
conn.execute(
some_table.insert(),
[
{"data": "some data one"},
{"data": "some data two"},
{"data": "some data three"}
]
)
# commits and closes automatically
Session::
Session = sessionmaker(engine, future=True)
with Session.begin() as session:
session.add_all([
SomeClass(data="some data one"),
SomeClass(data="some data two"),
SomeClass(data="some data three")
])
# commits and closes automatically
Nested Transaction
~~~~~~~~~~~~~~~~~~~~
When using a SAVEPOINT via the :meth:`_orm.Session.begin_nested` or
:meth:`_engine.Connection.begin_nested` methods, the transaction object
returned must be used to commit or rollback the SAVEPOINT. Calling
the :meth:`_orm.Session.commit` or :meth:`_future.Connection.commit` methods
will always commit the **outermost** transaction; this is a SQLAlchemy 2.0
specific behavior that is reversed from the 1.x series.
Engine::
engine = create_engine("postgresql://user:pass@host/dbname", future=True)
with engine.begin() as conn:
savepoint = conn.begin_nested()
conn.execute(
some_table.insert(),
[
{"data": "some data one"},
{"data": "some data two"},
{"data": "some data three"}
]
)
savepoint.commit() # or rollback
# commits automatically
Session::
Session = sessionmaker(engine, future=True)
with Session.begin() as session:
savepoint = session.begin_nested()
session.add_all([
SomeClass(data="some data one"),
SomeClass(data="some data two"),
SomeClass(data="some data three")
])
savepoint.commit() # or rollback
# commits automatically
.. _session_autocommit:
Autocommit Mode
.. _session_explicit_begin:
Explicit Begin
---------------
The examples of session lifecycle at :ref:`unitofwork_transaction` refer
to a :class:`.Session` that runs in its default mode of ``autocommit=False``.
In this mode, the :class:`.Session` begins new transactions automatically
as soon as it needs to do work upon a database connection; the transaction
then stays in progress until the :meth:`.Session.commit` or :meth:`.Session.rollback`
methods are called.
.. versionchanged:: 1.4
SQLAlchemy 1.4 deprecates "autocommit mode", which is historically enabled
by using the :paramref:`_orm.Session.autocommit` flag. This flag allows
the :class:`_orm.Session` to invoke SQL statements within individual,
ad-hoc transactions and has been recommended against for many years.
Instead, the :meth:`_orm.Session.begin` method may now be called when
a :class:`_orm.Session` is first constructed, or after the previous
transaction has ended and before it begins a new one.
The :class:`.Session` also features an older legacy mode of use called
**autocommit mode**, where a transaction is not started implicitly, and unless
the :meth:`.Session.begin` method is invoked, the :class:`.Session` will
perform each database operation on a new connection checked out from the
connection pool, which is then released back to the pool immediately
after the operation completes. This refers to
methods like :meth:`.Session.execute` as well as when executing a query
returned by :meth:`.Session.query`. For a flush operation, the :class:`.Session`
starts a new transaction for the duration of the flush, and commits it when
complete.
The :class:`_orm.Session` features "autobegin" behavior, meaning that as soon
as operations begin to take place, it ensures a :class:`_orm.SessionTransaction`
is present to track ongoing operations. This transaction is completed
when :meth:`_orm.Session.commit` is called.
.. warning::
It is often desirable, particularly in framework integrations, to control the
point at which the "begin" operation occurs. To suit this, the
:class:`_orm.Session` uses an "autobegin" strategy, such that the
:meth:`_orm.Session.begin` method may be called directly for a
:class:`_orm.Session` that has not already had a transaction begun::
"autocommit" mode is a **legacy mode of use** and should not be
considered for new projects. If autocommit mode is used, it is strongly
advised that the application at least ensure that transaction scope
is made present via the :meth:`.Session.begin` method, rather than
using the session in pure autocommit mode. An upcoming release of
SQLAlchemy will include a new mode of usage that provides this pattern
as a first class feature.
If the :meth:`.Session.begin` method is not used, and operations are allowed
to proceed using ad-hoc connections with immediate autocommit, then the
application probably should set ``autoflush=False, expire_on_commit=False``,
since these features are intended to be used only within the context
of a database transaction.
Modern usage of "autocommit mode" tends to be for framework integrations that
wish to control specifically when the "begin" state occurs. A session which is
configured with ``autocommit=True`` may be placed into the "begin" state using
the :meth:`.Session.begin` method. After the cycle completes upon
:meth:`.Session.commit` or :meth:`.Session.rollback`, connection and
transaction resources are :term:`released` and the :class:`.Session` goes back
into "autocommit" mode, until :meth:`.Session.begin` is called again::
Session = sessionmaker(bind=engine, autocommit=True)
Session = sessionmaker(bind=engine)
session = Session()
session.begin()
try:
@@ -198,10 +356,9 @@ into "autocommit" mode, until :meth:`.Session.begin` is called again::
session.rollback()
raise
The :meth:`.Session.begin` method also returns a transactional token which is
compatible with the ``with`` statement::
The above pattern is more idiomatically invoked using a context manager::
Session = sessionmaker(bind=engine, autocommit=True)
Session = sessionmaker(bind=engine)
session = Session()
with session.begin():
item1 = session.query(Item).get(1)
@@ -209,57 +366,98 @@ compatible with the ``with`` statement::
item1.foo = 'bar'
item2.bar = 'foo'
The :meth:`_orm.Session.begin` method and the session's "autobegin" process
use the same sequence of steps to begin the transaction. This includes
that the :meth:`_orm.SessionEvents.after_transaction_create` event is invoked
when it occurs; this hook is used by frameworks in order to integrate their
own trasactional processes with that of the ORM :class:`_orm.Session`.
.. _session_subtransactions:
Using Subtransactions with Autocommit
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Migrating from the "subtransaction" pattern
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A subtransaction indicates usage of the :meth:`.Session.begin` method in conjunction with
the ``subtransactions=True`` flag. This produces a non-transactional, delimiting construct that
allows nesting of calls to :meth:`~.Session.begin` and :meth:`~.Session.commit`.
Its purpose is to allow the construction of code that can function within a transaction
both independently of any external code that starts a transaction,
as well as within a block that has already demarcated a transaction.
.. deprecated:: 1.4 The :paramref:`_orm.Session.begin.subtransactions`
flag is deprecated. While the :class:`_orm.Session` still uses the
"subtransactions" pattern internally, it is not suitable for end-user
use as it leads to confusion, and additionally it may be removed from
the :class:`_orm.Session` itself in version 2.0 once "autocommit"
mode is removed.
The "subtransaction" pattern that was often used with autocommit mode is
also deprecated in 1.4. This pattern allowed the use of the
:meth:`_orm.Session.begin` method when a tranasction were already begun,
resulting in a construct called a "subtransaction", which was essentially
a block that would prevent the :meth:`_orm.Session.commit` method from actually
committing.
This pattern has been shown to be confusing in real world applications, and
it is preferable for an application to ensure that the top-most level of database
operations are performed with a single begin/commit pair.
To provide backwards compatibility for applications that make use of this
pattern, the following context manager or a similar implementation based on
a decorator may be used::
import contextlib
@contextlib.contextmanager
def transaction(session):
if session.in_transaction():
outermost = False
else:
outermost = True
session.begin()
try:
yield
except:
if session.in_transaction():
session.rollback()
raise
else:
if outermost and session.in_transaction():
session.commit()
The above context manager may be used in the same way the
"subtransaction" flag works, such as in the following example::
``subtransactions=True`` is generally only useful in conjunction with
autocommit, and is equivalent to the pattern described at :ref:`connections_nested_transactions`,
where any number of functions can call :meth:`_engine.Connection.begin` and :meth:`.Transaction.commit`
as though they are the initiator of the transaction, but in fact may be participating
in an already ongoing transaction::
# method_a starts a transaction and calls method_b
def method_a(session):
session.begin(subtransactions=True)
try:
with transaction(session):
method_b(session)
session.commit() # transaction is committed here
except:
session.rollback() # rolls back the transaction
raise
# method_b also starts a transaction, but when
# called from method_a participates in the ongoing
# transaction.
def method_b(session):
session.begin(subtransactions=True)
try:
with transaction(session):
session.add(SomeObject('bat', 'lala'))
session.commit() # transaction is not committed yet
except:
session.rollback() # rolls back the transaction, in this case
# the one that was initiated in method_a().
raise
# create a Session and call method_a
session = Session(autocommit=True)
method_a(session)
session.close()
with Session() as session:
method_a(session)
To compare towards the preferred idiomatic pattern, the begin block should
be at the outermost level. This removes the need for individual functions
or methods to be concerned with the details of transaction demarcation::
def method_a(session):
method_b(session)
def method_b(session):
session.add(SomeObject('bat', 'lala'))
# create a Session and call method_a
with Session() as session:
with session.begin():
method_a(session)
Subtransactions are used by the :meth:`.Session.flush` process to ensure that the
flush operation takes place within a transaction, regardless of autocommit. When
autocommit is disabled, it is still useful in that it forces the :class:`.Session`
into a "pending rollback" state, as a failed flush cannot be resumed in mid-operation,
where the end user still maintains the "scope" of the transaction overall.
.. _session_twophase:
@@ -270,7 +468,7 @@ For backends which support two-phase operation (currently MySQL and
PostgreSQL), the session can be instructed to use two-phase commit semantics.
This will coordinate the committing of transactions across databases so that
the transaction is either committed or rolled back in all databases. You can
also :meth:`~.Session.prepare` the session for
also :meth:`_orm.Session.prepare` the session for
interacting with transactions not managed by SQLAlchemy. To use two phase
transactions set the flag ``twophase=True`` on the session::
@@ -417,21 +615,6 @@ affect how the bind is procured::
# and reverted to its previous isolation level.
sess.commit()
The :paramref:`.Session.connection.execution_options` argument is only
accepted on the **first** call to :meth:`.Session.connection` for a
particular bind within a transaction. If a transaction is already begun
on the target connection, a warning is emitted::
>>> session = Session(eng)
>>> session.execute("select 1")
<sqlalchemy.engine.result.CursorResult object at 0x1017a6c50>
>>> session.connection(execution_options={'isolation_level': 'SERIALIZABLE'})
sqlalchemy/orm/session.py:310: SAWarning: Connection is already established
for the given bind; execution_options ignored
.. versionadded:: 0.9.9 Added the
:paramref:`.Session.connection.execution_options`
parameter to :meth:`.Session.connection`.
Tracking Transaction State with Events
--------------------------------------
@@ -450,7 +633,22 @@ be made to participate within that transaction by just binding the
:class:`.Session` to that :class:`_engine.Connection`. The usual rationale for this
is a test suite that allows ORM code to work freely with a :class:`.Session`,
including the ability to call :meth:`.Session.commit`, where afterwards the
entire database interaction is rolled back::
entire database interaction is rolled back.
.. versionchanged:: 1.4 This section introduces a new version of the
"join into an external transaction" recipe that will work equally well
for both "future" and "non-future" engines and sessions. The recipe
here from previous versions such as 1.3 will also continue to work for
"non-future" engines and sessions.
The recipe works by establishing a :class:`_engine.Connection` within a
transaction and optionally a SAVEPOINT, then passing it to a :class:`_orm.Session` as the
"bind". The :class:`_orm.Session` detects that the given :class:`_engine.Connection`
is already in a transaction and will not run COMMIT on it if the transaction
is in fact an outermost transaction. Then when the test tears down, the
transaction is rolled back so that any data changes throughout the test
are reverted::
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
@@ -469,15 +667,43 @@ entire database interaction is rolled back::
# begin a non-ORM transaction
self.trans = self.connection.begin()
# bind an individual Session to the connection
self.session = Session(bind=self.connection)
### optional ###
# if the database supports SAVEPOINT (SQLite needs special
# config for this to work), starting a savepoint
# will allow tests to also use rollback within tests
self.nested = self.connection.begin_nested()
@event.listens_for(self.session, "after_transaction_end")
def end_savepoint(session, transaction):
if not self.nested.is_active:
self.nested = self.connection.begin_nested()
### ^^^ optional ^^^ ###
def test_something(self):
# use the session in tests.
self.session.add(Foo())
self.session.commit()
def test_something_with_rollbacks(self):
# if the SAVEPOINT steps are taken, then a test can also
# use session.rollback() and continue working with the database
self.session.add(Bar())
self.session.flush()
self.session.rollback()
self.session.add(Foo())
self.session.commit()
def tearDown(self):
self.session.close()
@@ -489,53 +715,6 @@ entire database interaction is rolled back::
# return connection to the Engine
self.connection.close()
Above, we issue :meth:`.Session.commit` as well as
:meth:`.Transaction.rollback`. This is an example of where we take advantage
of the :class:`_engine.Connection` object's ability to maintain *subtransactions*, or
nested begin/commit-or-rollback pairs where only the outermost begin/commit
pair actually commits the transaction, or if the outermost block rolls back,
everything is rolled back.
.. topic:: Supporting Tests with Rollbacks
The above recipe works well for any kind of database enabled test, except
for a test that needs to actually invoke :meth:`.Session.rollback` within
the scope of the test itself. The above recipe can be expanded, such
that the :class:`.Session` always runs all operations within the scope
of a SAVEPOINT, which is established at the start of each transaction,
so that tests can also rollback the "transaction" as well while still
remaining in the scope of a larger "transaction" that's never committed,
using two extra events::
from sqlalchemy import event
class SomeTest(TestCase):
def setUp(self):
# connect to the database
self.connection = engine.connect()
# begin a non-ORM transaction
self.trans = connection.begin()
# bind an individual Session to the connection
self.session = Session(bind=self.connection)
# start the session in a SAVEPOINT...
self.session.begin_nested()
# then each time that SAVEPOINT ends, reopen it
@event.listens_for(self.session, "after_transaction_end")
def restart_savepoint(session, transaction):
if transaction.nested and not transaction._parent.nested:
# ensure that state is expired the way
# session.commit() at the top level normally does
# (optional step)
session.expire_all()
session.begin_nested()
# ... the tearDown() method stays the same
The above recipe is part of SQLAlchemy's own CI to ensure that it remains
working as expected.
+34
View File
@@ -759,6 +759,40 @@ class Connection(Connectable):
return self._transaction is not None and self._transaction.is_active
def in_nested_transaction(self):
"""Return True if a transaction is in progress."""
if self.__branch_from is not None:
return self.__branch_from.in_nested_transaction()
return (
self._nested_transaction is not None
and self._nested_transaction.is_active
)
def get_transaction(self):
"""Return the current root transaction in progress, if any.
.. versionadded:: 1.4
"""
if self.__branch_from is not None:
return self.__branch_from.get_transaction()
return self._transaction
def get_nested_transaction(self):
"""Return the current nested transaction in progress, if any.
.. versionadded:: 1.4
"""
if self.__branch_from is not None:
return self.__branch_from.get_nested_transaction()
return self._nested_transaction
def _begin_impl(self, transaction):
assert not self.__branch_from
+2 -1
View File
@@ -282,7 +282,8 @@ class Result(InPlaceGenerative):
updated usage model and calling facade for SQLAlchemy Core and
SQLAlchemy ORM. In Core, it forms the basis of the
:class:`.CursorResult` object which replaces the previous
:class:`.ResultProxy` interface.
:class:`.ResultProxy` interface. When using the ORM, a higher level
object called :class:`.ChunkedIteratorResult` is normally used.
"""
+1 -1
View File
@@ -234,7 +234,7 @@ class BakedQuery(object):
# used by the Connection, which in itself is more expensive to
# generate than what BakedQuery was able to provide in 1.3 and prior
if statement.compile_options._bake_ok:
if statement._compile_options._bake_ok:
self._bakery[self._effective_key(session)] = (
query,
statement,
+9 -1
View File
@@ -90,8 +90,13 @@ def create_session(bind=None, **kwargs):
create_session().
"""
if kwargs.get("future", False):
kwargs.setdefault("autocommit", False)
else:
kwargs.setdefault("autocommit", True)
kwargs.setdefault("autoflush", False)
kwargs.setdefault("autocommit", True)
kwargs.setdefault("expire_on_commit", False)
return Session(bind=bind, **kwargs)
@@ -267,12 +272,15 @@ contains_alias = public_factory(AliasOption, ".orm.contains_alias")
def __go(lcls):
global __all__
global AppenderQuery
from .. import util as sa_util # noqa
from . import dynamic # noqa
from . import events # noqa
from . import loading # noqa
import inspect as _inspect
from .dynamic import AppenderQuery
__all__ = sorted(
name
for name, obj in lcls.items()
+43 -16
View File
@@ -188,7 +188,7 @@ class ORMCompileState(CompileState):
raise NotImplementedError()
@classmethod
def get_column_descriptions(self, statement):
def get_column_descriptions(cls, statement):
return _column_descriptions(statement)
@classmethod
@@ -204,8 +204,14 @@ class ORMCompileState(CompileState):
if is_reentrant_invoke:
return statement, execution_options
load_options = execution_options.get(
"_sa_orm_load_options", QueryContext.default_load_options
(
load_options,
execution_options,
) = QueryContext.default_load_options.from_execution_options(
"_sa_orm_load_options",
{"populate_existing", "autoflush", "yield_per"},
execution_options,
statement._execution_options,
)
bind_arguments["clause"] = statement
@@ -246,6 +252,7 @@ class ORMCompileState(CompileState):
load_options = execution_options.get(
"_sa_orm_load_options", QueryContext.default_load_options
)
querycontext = QueryContext(
compile_state,
statement,
@@ -304,7 +311,7 @@ class ORMFromStatementCompileState(ORMCompileState):
self._primary_entity = None
self.use_legacy_query_style = (
statement_container.compile_options._use_legacy_query_style
statement_container._compile_options._use_legacy_query_style
)
self.statement_container = self.select_statement = statement_container
self.requested_statement = statement = statement_container.element
@@ -315,9 +322,9 @@ class ORMFromStatementCompileState(ORMCompileState):
_QueryEntity.to_compile_state(self, statement_container._raw_columns)
self.compile_options = statement_container.compile_options
self.compile_options = statement_container._compile_options
self.current_path = statement_container.compile_options._current_path
self.current_path = statement_container._compile_options._current_path
if toplevel and statement_container._with_options:
self.attributes = {"_unbound_load_dedupes": set()}
@@ -416,8 +423,8 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
# if we are a select() that was never a legacy Query, we won't
# have ORM level compile options.
statement.compile_options = cls.default_compile_options.safe_merge(
statement.compile_options
statement._compile_options = cls.default_compile_options.safe_merge(
statement._compile_options
)
self = cls.__new__(cls)
@@ -434,20 +441,20 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
# indicates this select() came from Query.statement
self.for_statement = (
for_statement
) = select_statement.compile_options._for_statement
) = select_statement._compile_options._for_statement
if not for_statement and not toplevel:
# for subqueries, turn off eagerloads.
# if "for_statement" mode is set, Query.subquery()
# would have set this flag to False already if that's what's
# desired
select_statement.compile_options += {
select_statement._compile_options += {
"_enable_eagerloads": False,
}
# generally if we are from Query or directly from a select()
self.use_legacy_query_style = (
select_statement.compile_options._use_legacy_query_style
select_statement._compile_options._use_legacy_query_style
)
self._entities = []
@@ -457,15 +464,15 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
self._no_yield_pers = set()
# legacy: only for query.with_polymorphic()
if select_statement.compile_options._with_polymorphic_adapt_map:
if select_statement._compile_options._with_polymorphic_adapt_map:
self._with_polymorphic_adapt_map = dict(
select_statement.compile_options._with_polymorphic_adapt_map
select_statement._compile_options._with_polymorphic_adapt_map
)
self._setup_with_polymorphics()
_QueryEntity.to_compile_state(self, select_statement._raw_columns)
self.compile_options = select_statement.compile_options
self.compile_options = select_statement._compile_options
# determine label style. we can make different decisions here.
# at the moment, trying to see if we can always use DISAMBIGUATE_ONLY
@@ -479,7 +486,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
else:
self.label_style = self.select_statement._label_style
self.current_path = select_statement.compile_options._current_path
self.current_path = select_statement._compile_options._current_path
self.eager_order_by = ()
@@ -668,7 +675,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
self._polymorphic_adapters = {}
compile_options = cls.default_compile_options.safe_merge(
query.compile_options
query._compile_options
)
# legacy: only for query.with_polymorphic()
if compile_options._with_polymorphic_adapt_map:
@@ -711,6 +718,26 @@ class ORMSelectCompileState(ORMCompileState, SelectState):
for elem in _select_iterables([element]):
yield elem
@classmethod
@util.preload_module("sqlalchemy.orm.query")
def from_statement(cls, statement, from_statement):
query = util.preloaded.orm_query
from_statement = coercions.expect(
roles.SelectStatementRole,
from_statement,
apply_propagate_attrs=statement,
)
stmt = query.FromStatement(statement._raw_columns, from_statement)
stmt.__dict__.update(
_with_options=statement._with_options,
_with_context_options=statement._with_context_options,
_execution_options=statement._execution_options,
_propagate_attrs=statement._propagate_attrs,
)
return stmt
def _setup_with_polymorphics(self):
# legacy: only for query.with_polymorphic()
for ext_info, wp in self._with_polymorphic_adapt_map.items():
+243 -75
View File
@@ -23,7 +23,12 @@ from . import util as orm_util
from .query import Query
from .. import exc
from .. import log
from .. import sql
from .. import util
from ..engine import result as _result
from ..sql import selectable
from ..sql.base import _generative
from ..sql.base import Generative
@log.class_logger
@@ -74,7 +79,6 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
dispatch,
target_mapper,
order_by,
query_class=None,
**kw
):
super(DynamicAttributeImpl, self).__init__(
@@ -82,12 +86,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
)
self.target_mapper = target_mapper
self.order_by = order_by
if not query_class:
self.query_class = AppenderQuery
elif AppenderMixin in query_class.mro():
self.query_class = query_class
else:
self.query_class = mixin_user_query(query_class)
self.query_class = AppenderQuery
def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
if not passive & attributes.SQL_OK:
@@ -259,15 +258,26 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
self.remove(state, dict_, value, initiator, passive=passive)
class AppenderMixin(object):
query_class = None
class AppenderQuery(Generative):
"""A dynamic query that supports basic collection storage operations."""
def __init__(self, attr, state):
super(AppenderMixin, self).__init__(attr.target_mapper, None)
# this can be select() except for aliased=True flag on join()
# and corresponding behaviors on select().
self._is_core = False
self._statement = Query([attr.target_mapper], None)
# self._is_core = True
# self._statement = sql.select(attr.target_mapper)._set_label_style(
# selectable.LABEL_STYLE_TABLENAME_PLUS_COL
# )
self._autoflush = True
self.instance = instance = state.obj()
self.attr = attr
mapper = object_mapper(instance)
self.mapper = mapper = object_mapper(instance)
prop = mapper._props[self.attr.key]
if prop.secondary is not None:
@@ -277,29 +287,154 @@ class AppenderMixin(object):
# is in the FROM. So we purposely put the mapper selectable
# in _from_obj[0] to ensure a user-defined join() later on
# doesn't fail, and secondary is then in _from_obj[1].
self._from_obj = (prop.mapper.selectable, prop.secondary)
self._statement = self._statement.select_from(
prop.mapper.selectable, prop.secondary
)
self._where_criteria += (
self._statement = self._statement.where(
prop._with_parent(instance, alias_secondary=False),
)
if self.attr.order_by:
if (
self._order_by_clauses is False
or self._order_by_clauses is None
):
self._order_by_clauses = tuple(self.attr.order_by)
else:
self._order_by_clauses = self._order_by_clauses + tuple(
self.attr.order_by
)
self._statement = self._statement.order_by(*self.attr.order_by)
@_generative
def autoflush(self, setting):
"""Set autoflush to a specific setting.
Note that a Session with autoflush=False will
not autoflush, even if this flag is set to True at the
Query level. Therefore this flag is usually used only
to disable autoflush for a specific Query.
"""
self._autoflush = setting
@property
def statement(self):
"""Return the Core statement represented by this
:class:`.AppenderQuery`.
"""
if self._is_core:
return self._statement._set_label_style(
selectable.LABEL_STYLE_DISAMBIGUATE_ONLY
)
else:
return self._statement.statement
def filter(self, *criteria):
"""A synonym for the :meth:`_orm.AppenderQuery.where` method."""
return self.where(*criteria)
@_generative
def where(self, *criteria):
r"""Apply the given WHERE criterion, using SQL expressions.
Equivalent to :meth:`.Select.where`.
"""
self._statement = self._statement.where(*criteria)
@_generative
def order_by(self, *criteria):
r"""Apply the given ORDER BY criterion, using SQL expressions.
Equivalent to :meth:`.Select.order_by`.
"""
self._statement = self._statement.order_by(*criteria)
@_generative
def filter_by(self, **kwargs):
r"""Apply the given filtering criterion using keyword expressions.
Equivalent to :meth:`.Select.filter_by`.
"""
self._statement = self._statement.filter_by(**kwargs)
@_generative
def join(self, target, *props, **kwargs):
r"""Create a SQL JOIN against this
object's criterion.
Equivalent to :meth:`.Select.join`.
"""
self._statement = self._statement.join(target, *props, **kwargs)
@_generative
def outerjoin(self, target, *props, **kwargs):
r"""Create a SQL LEFT OUTER JOIN against this
object's criterion.
Equivalent to :meth:`.Select.outerjoin`.
"""
self._statement = self._statement.outerjoin(target, *props, **kwargs)
def scalar(self):
"""Return the first element of the first result or None
if no rows present. If multiple rows are returned,
raises MultipleResultsFound.
Equivalent to :meth:`_query.Query.scalar`.
.. versionadded:: 1.1.6
"""
return self._iter().scalar()
def first(self):
"""Return the first row.
Equivalent to :meth:`_query.Query.first`.
"""
# replicates limit(1) behavior
if self._statement is not None:
return self._iter().first()
else:
return self.limit(1)._iter().first()
def one(self):
"""Return exactly one result or raise an exception.
Equivalent to :meth:`_query.Query.one`.
"""
return self._iter().one()
def one_or_none(self):
"""Return one or zero results, or raise an exception for multiple
rows.
Equivalent to :meth:`_query.Query.one_or_none`.
.. versionadded:: 1.0.9
"""
return self._iter().one_or_none()
def all(self):
"""Return all rows.
Equivalent to :meth:`_query.Query.all`.
"""
return self._iter().all()
def session(self):
sess = object_session(self.instance)
if (
sess is not None
and self.autoflush
and self._autoflush
and sess.autoflush
and self.instance in sess
):
@@ -311,41 +446,7 @@ class AppenderMixin(object):
session = property(session, lambda s, x: None)
def __iter__(self):
sess = self.session
if sess is None:
return iter(
self.attr._get_collection_history(
attributes.instance_state(self.instance),
attributes.PASSIVE_NO_INITIALIZE,
).added_items
)
else:
return iter(self._generate(sess))
def __getitem__(self, index):
sess = self.session
if sess is None:
return self.attr._get_collection_history(
attributes.instance_state(self.instance),
attributes.PASSIVE_NO_INITIALIZE,
).indexed(index)
else:
return self._generate(sess).__getitem__(index)
def count(self):
sess = self.session
if sess is None:
return len(
self.attr._get_collection_history(
attributes.instance_state(self.instance),
attributes.PASSIVE_NO_INITIALIZE,
).added_items
)
else:
return self._generate(sess).count()
def _generate(self, sess=None):
def _execute(self, sess=None):
# note we're returning an entirely new Query class instance
# here without any assignment capabilities; the class of this
# query is determined by the session.
@@ -360,16 +461,93 @@ class AppenderMixin(object):
% (orm_util.instance_str(instance), self.attr.key)
)
if self.query_class:
query = self.query_class(self.attr.target_mapper, session=sess)
result = sess.execute(self._statement, future=True)
result = result.scalars()
if result._attributes.get("filtered", False):
result = result.unique()
return result
def _iter(self):
sess = self.session
if sess is None:
instance = self.instance
state = attributes.instance_state(instance)
if state.detached:
raise orm_exc.DetachedInstanceError(
"Parent instance %s is not bound to a Session, and no "
"contextual session is established; lazy load operation "
"of attribute '%s' cannot proceed"
% (orm_util.instance_str(instance), self.attr.key)
)
else:
iterator = (
(item,)
for item in self.attr._get_collection_history(
state, attributes.PASSIVE_NO_INITIALIZE,
).added_items
)
row_metadata = _result.SimpleResultMetaData(
(self.mapper.class_.__name__,), [], _unique_filters=[id],
)
return _result.IteratorResult(row_metadata, iterator).scalars()
else:
query = sess.query(self.attr.target_mapper)
return self._execute(sess)
query._where_criteria = self._where_criteria
query._from_obj = self._from_obj
query._order_by_clauses = self._order_by_clauses
def __iter__(self):
return iter(self._iter())
return query
def __getitem__(self, index):
sess = self.session
if sess is None:
return self.attr._get_collection_history(
attributes.instance_state(self.instance),
attributes.PASSIVE_NO_INITIALIZE,
).indexed(index)
else:
return orm_util._getitem(self, index)
def slice(self, start, stop):
"""Computes the "slice" represented by
the given indices and apply as LIMIT/OFFSET.
"""
limit_clause, offset_clause = orm_util._make_slice(
self._statement._limit_clause,
self._statement._offset_clause,
start,
stop,
)
self._statement = self._statement.limit(limit_clause).offset(
offset_clause
)
def count(self):
"""return the 'count'.
Equivalent to :meth:`_query.Query.count`.
"""
sess = self.session
if sess is None:
return len(
self.attr._get_collection_history(
attributes.instance_state(self.instance),
attributes.PASSIVE_NO_INITIALIZE,
).added_items
)
else:
col = sql.func.count(sql.literal_column("*"))
stmt = sql.select(col).select_from(self._statement.subquery())
return self.session.execute(stmt).scalar()
def extend(self, iterator):
for item in iterator:
@@ -397,16 +575,6 @@ class AppenderMixin(object):
)
class AppenderQuery(AppenderMixin, Query):
"""A dynamic query that supports basic collection storage operations."""
def mixin_user_query(cls):
"""Return a new class with AppenderQuery functionality layered over."""
name = "Appender" + cls.__name__
return type(name, (AppenderMixin, cls), {"query_class": cls})
class CollectionHistory(object):
"""Overrides AttributeHistory to receive append/remove events directly."""
+2 -2
View File
@@ -346,7 +346,7 @@ def load_on_pk_identity(
load_options = QueryContext.default_load_options
compile_options = ORMCompileState.default_compile_options.safe_merge(
q.compile_options
q._compile_options
)
if primary_key_identity is not None:
@@ -411,7 +411,7 @@ def load_on_pk_identity(
# TODO: most of the compile_options that are not legacy only involve this
# function, so try to see if handling of them can mostly be local to here
q.compile_options, load_options = _set_get_options(
q._compile_options, load_options = _set_get_options(
compile_options,
load_options,
populate_existing=bool(refresh_state),
+8 -9
View File
@@ -1762,24 +1762,23 @@ class BulkUDCompileState(CompileState):
if is_reentrant_invoke:
return statement, execution_options
sync = execution_options.get("synchronize_session", None)
if sync is None:
sync = statement._execution_options.get(
"synchronize_session", None
)
update_options = execution_options.get(
(
update_options,
execution_options,
) = BulkUDCompileState.default_update_options.from_execution_options(
"_sa_orm_update_options",
BulkUDCompileState.default_update_options,
{"synchronize_session"},
execution_options,
statement._execution_options,
)
sync = update_options._synchronize_session
if sync is not None:
if sync not in ("evaluate", "fetch", False):
raise sa_exc.ArgumentError(
"Valid strategies for session synchronization "
"are 'evaluate', 'fetch', False"
)
update_options += {"_synchronize_session": sync}
bind_arguments["clause"] = statement
try:
+70 -376
View File
@@ -22,10 +22,10 @@ import itertools
import operator
import types
from . import attributes
from . import exc as orm_exc
from . import interfaces
from . import loading
from . import util as orm_util
from .base import _assertions
from .context import _column_descriptions
from .context import _legacy_determine_last_joined_entity
@@ -121,7 +121,7 @@ class Query(
_legacy_setup_joins = ()
_label_style = LABEL_STYLE_NONE
compile_options = ORMCompileState.default_compile_options
_compile_options = ORMCompileState.default_compile_options
load_options = QueryContext.default_load_options
@@ -215,7 +215,7 @@ class Query(
for elem in obj
]
self.compile_options += {"_set_base_alias": set_base_alias}
self._compile_options += {"_set_base_alias": set_base_alias}
self._from_obj = tuple(fa)
@_generative
@@ -254,7 +254,7 @@ class Query(
self._from_obj = self._legacy_setup_joins = ()
if self._statement is not None:
self.compile_options += {"_statement": None}
self._compile_options += {"_statement": None}
self._where_criteria = ()
self._distinct = False
@@ -320,7 +320,7 @@ class Query(
if load_options:
self.load_options += load_options
if compile_options:
self.compile_options += compile_options
self._compile_options += compile_options
return self
@@ -357,8 +357,8 @@ class Query(
# passed into the execute process and wont generate its own cache
# key; this will all occur in terms of the ORM-enabled Select.
if (
not self.compile_options._set_base_alias
and not self.compile_options._with_polymorphic_adapt_map
not self._compile_options._set_base_alias
and not self._compile_options._with_polymorphic_adapt_map
):
# if we don't have legacy top level aliasing features in use
# then convert to a future select() directly
@@ -400,9 +400,9 @@ class Query(
if new_query is not None and new_query is not self:
self = new_query
if not fn._bake_ok:
self.compile_options += {"_bake_ok": False}
self._compile_options += {"_bake_ok": False}
compile_options = self.compile_options
compile_options = self._compile_options
compile_options += {
"_for_statement": for_statement,
"_use_legacy_query_style": use_legacy_query_style,
@@ -413,21 +413,21 @@ class Query(
stmt.__dict__.update(
_with_options=self._with_options,
_with_context_options=self._with_context_options,
compile_options=compile_options,
_compile_options=compile_options,
_execution_options=self._execution_options,
_propagate_attrs=self._propagate_attrs,
)
stmt._propagate_attrs = self._propagate_attrs
else:
# Query / select() internal attributes are 99% cross-compatible
stmt = Select.__new__(Select)
stmt.__dict__.update(self.__dict__)
stmt.__dict__.update(
_label_style=self._label_style,
compile_options=compile_options,
_compile_options=compile_options,
_propagate_attrs=self._propagate_attrs,
)
stmt.__dict__.pop("session", None)
stmt._propagate_attrs = self._propagate_attrs
return stmt
def subquery(
@@ -629,7 +629,7 @@ class Query(
selectable, or when using :meth:`_query.Query.yield_per`.
"""
self.compile_options += {"_enable_eagerloads": value}
self._compile_options += {"_enable_eagerloads": value}
@_generative
def with_labels(self):
@@ -710,7 +710,7 @@ class Query(
query intended for the deferred load.
"""
self.compile_options += {"_current_path": path}
self._compile_options += {"_current_path": path}
# TODO: removed in 2.0
@_generative
@@ -744,7 +744,7 @@ class Query(
polymorphic_on=polymorphic_on,
)
self.compile_options = self.compile_options.add_to_element(
self._compile_options = self._compile_options.add_to_element(
"_with_polymorphic_adapt_map", ((entity, inspect(wp)),)
)
@@ -818,6 +818,10 @@ class Query(
{"stream_results": True, "max_row_buffer": count}
)
@util.deprecated_20(
":meth:`_orm.Query.get`",
alternative="The method is now available as :meth:`_orm.Session.get`",
)
def get(self, ident):
"""Return an instance based on the given primary key identifier,
or ``None`` if not found.
@@ -858,14 +862,6 @@ class Query(
however, and will be used if the object is not
yet locally present.
A lazy-loading, many-to-one attribute configured
by :func:`_orm.relationship`, using a simple
foreign-key-to-primary-key criterion, will also use an
operation equivalent to :meth:`_query.Query.get` in order to retrieve
the target value from the local identity map
before querying the database. See :doc:`/orm/loading_relationships`
for further details on relationship loading.
:param ident: A scalar, tuple, or dictionary representing the
primary key. For a composite (e.g. multiple column) primary key,
a tuple or dictionary should be passed.
@@ -905,80 +901,22 @@ class Query(
"""
self._no_criterion_assertion("get", order_by=False, distinct=False)
# we still implement _get_impl() so that baked query can override
# it
return self._get_impl(ident, loading.load_on_pk_identity)
def _get_impl(self, primary_key_identity, db_load_fn, identity_token=None):
# convert composite types to individual args
if hasattr(primary_key_identity, "__composite_values__"):
primary_key_identity = primary_key_identity.__composite_values__()
mapper = self._only_full_mapper_zero("get")
is_dict = isinstance(primary_key_identity, dict)
if not is_dict:
primary_key_identity = util.to_list(
primary_key_identity, default=(None,)
)
if len(primary_key_identity) != len(mapper.primary_key):
raise sa_exc.InvalidRequestError(
"Incorrect number of values in identifier to formulate "
"primary key for query.get(); primary key columns are %s"
% ",".join("'%s'" % c for c in mapper.primary_key)
)
if is_dict:
try:
primary_key_identity = list(
primary_key_identity[prop.key]
for prop in mapper._identity_key_props
)
except KeyError as err:
util.raise_(
sa_exc.InvalidRequestError(
"Incorrect names of values in identifier to formulate "
"primary key for query.get(); primary key attribute "
"names are %s"
% ",".join(
"'%s'" % prop.key
for prop in mapper._identity_key_props
)
),
replace_context=err,
)
if (
not self.load_options._populate_existing
and not mapper.always_refresh
and self._for_update_arg is None
):
instance = self.session._identity_lookup(
mapper, primary_key_identity, identity_token=identity_token
)
if instance is not None:
self._get_existing_condition()
# reject calls for id in identity map but class
# mismatch.
if not issubclass(instance.__class__, mapper.class_):
return None
return instance
elif instance is attributes.PASSIVE_CLASS_MISMATCH:
return None
# apply_labels() not strictly necessary, however this will ensure that
# tablename_colname style is used which at the moment is asserted
# in a lot of unit tests :)
statement = self._statement_20().apply_labels()
return db_load_fn(
self.session,
statement,
return self.session._get_impl(
mapper,
primary_key_identity,
load_options=self.load_options,
db_load_fn,
populate_existing=self.load_options._populate_existing,
with_for_update=self._for_update_arg,
options=self._with_options,
identity_token=identity_token,
execution_options=self._execution_options,
)
@property
@@ -1000,7 +938,7 @@ class Query(
@property
def _current_path(self):
return self.compile_options._current_path
return self._compile_options._current_path
@_generative
def correlate(self, *fromclauses):
@@ -1375,7 +1313,7 @@ class Query(
@_generative
def _set_enable_single_crit(self, val):
self.compile_options += {"_enable_single_crit": val}
self._compile_options += {"_enable_single_crit": val}
@_generative
def _from_selectable(self, fromclause, set_entity_from=True):
@@ -1394,7 +1332,7 @@ class Query(
):
self.__dict__.pop(attr, None)
self._set_select_from([fromclause], set_entity_from)
self.compile_options += {
self._compile_options += {
"_enable_single_crit": False,
"_statement": None,
}
@@ -1404,7 +1342,7 @@ class Query(
# legacy. see test/orm/test_froms.py for various
# "oldstyle" tests that rely on this and the correspoinding
# "newtyle" that do not.
self.compile_options += {"_orm_only_from_obj_alias": False}
self._compile_options += {"_orm_only_from_obj_alias": False}
@util.deprecated(
"1.4",
@@ -1517,7 +1455,7 @@ class Query(
"""
opts = tuple(util.flatten_iterator(args))
if self.compile_options._current_path:
if self._compile_options._current_path:
for opt in opts:
if opt._is_legacy_option:
opt.process_query_conditionally(self)
@@ -1641,6 +1579,14 @@ class Query(
params = self.load_options._params.union(kwargs)
self.load_options += {"_params": params}
def where(self, *criterion):
"""A synonym for :meth:`.Query.filter`.
.. versionadded:: 1.4
"""
return self.filter(*criterion)
@_generative
@_assertions(_no_statement_condition, _no_limit_offset)
def filter(self, *criterion):
@@ -2204,6 +2150,7 @@ class Query(
SQLAlchemy versions was the primary ORM-level joining interface.
"""
aliased, from_joinpoint, isouter, full = (
kwargs.pop("aliased", False),
kwargs.pop("from_joinpoint", False),
@@ -2496,36 +2443,10 @@ class Query(
"""
self._set_select_from([from_obj], True)
self.compile_options += {"_enable_single_crit": False}
self._compile_options += {"_enable_single_crit": False}
def __getitem__(self, item):
if isinstance(item, slice):
start, stop, step = util.decode_slice(item)
if (
isinstance(stop, int)
and isinstance(start, int)
and stop - start <= 0
):
return []
# perhaps we should execute a count() here so that we
# can still use LIMIT/OFFSET ?
elif (isinstance(start, int) and start < 0) or (
isinstance(stop, int) and stop < 0
):
return list(self)[item]
res = self.slice(start, stop)
if step is not None:
return list(res)[None : None : item.step]
else:
return list(res)
else:
if item == -1:
return list(self)[-1]
else:
return list(self[item : item + 1])[0]
return orm_util._getitem(self, item)
@_generative
@_assertions(_no_statement_condition)
@@ -2559,46 +2480,10 @@ class Query(
:meth:`_query.Query.offset`
"""
# for calculated limit/offset, try to do the addition of
# values to offset in Python, howver if a SQL clause is present
# then the addition has to be on the SQL side.
if start is not None and stop is not None:
offset_clause = self._offset_or_limit_clause_asint_if_possible(
self._offset_clause
)
if offset_clause is None:
offset_clause = 0
if start != 0:
offset_clause = offset_clause + start
if offset_clause == 0:
self._offset_clause = None
else:
self._offset_clause = self._offset_or_limit_clause(
offset_clause
)
self._limit_clause = self._offset_or_limit_clause(stop - start)
elif start is None and stop is not None:
self._limit_clause = self._offset_or_limit_clause(stop)
elif start is not None and stop is None:
offset_clause = self._offset_or_limit_clause_asint_if_possible(
self._offset_clause
)
if offset_clause is None:
offset_clause = 0
if start != 0:
offset_clause = offset_clause + start
if offset_clause == 0:
self._offset_clause = None
else:
self._offset_clause = self._offset_or_limit_clause(
offset_clause
)
self._limit_clause, self._offset_clause = orm_util._make_slice(
self._limit_clause, self._offset_clause, start, stop
)
@_generative
@_assertions(_no_statement_condition)
@@ -2607,7 +2492,7 @@ class Query(
``Query``.
"""
self._limit_clause = self._offset_or_limit_clause(limit)
self._limit_clause = orm_util._offset_or_limit_clause(limit)
@_generative
@_assertions(_no_statement_condition)
@@ -2616,31 +2501,7 @@ class Query(
``Query``.
"""
self._offset_clause = self._offset_or_limit_clause(offset)
def _offset_or_limit_clause(self, element, name=None, type_=None):
"""Convert the given value to an "offset or limit" clause.
This handles incoming integers and converts to an expression; if
an expression is already given, it is passed through.
"""
return coercions.expect(
roles.LimitOffsetRole, element, name=name, type_=type_
)
def _offset_or_limit_clause_asint_if_possible(self, clause):
"""Return the offset or limit clause as a simple integer if possible,
else return the clause.
"""
if clause is None:
return None
if hasattr(clause, "_limit_offset_value"):
value = clause._limit_offset_value
return util.asint(value)
else:
return clause
self._offset_clause = orm_util._offset_or_limit_clause(offset)
@_generative
@_assertions(_no_statement_condition)
@@ -2723,7 +2584,7 @@ class Query(
roles.SelectStatementRole, statement, apply_propagate_attrs=self
)
self._statement = statement
self.compile_options += {"_statement": statement}
self._compile_options += {"_statement": statement}
def first(self):
"""Return the first result of this ``Query`` or
@@ -3088,110 +2949,22 @@ class Query(
sess.query(User).filter(User.age == 25).\
delete(synchronize_session='evaluate')
.. warning:: The :meth:`_query.Query.delete`
method is a "bulk" operation,
which bypasses ORM unit-of-work automation in favor of greater
performance. **Please read all caveats and warnings below.**
.. warning::
:param synchronize_session: chooses the strategy for the removal of
matched objects from the session. Valid values are:
See the section :ref:`bulk_update_delete` for important caveats
and warnings, including limitations when using bulk UPDATE
and DELETE with mapper inheritance configurations.
``False`` - don't synchronize the session. This option is the most
efficient and is reliable once the session is expired, which
typically occurs after a commit(), or explicitly using
expire_all(). Before the expiration, objects may still remain in
the session which were in fact deleted which can lead to confusing
results if they are accessed via get() or already loaded
collections.
``'fetch'`` - performs a select query before the delete to find
objects that are matched by the delete query and need to be
removed from the session. Matched objects are removed from the
session.
``'evaluate'`` - Evaluate the query's criteria in Python straight
on the objects in the session. If evaluation of the criteria isn't
implemented, an error is raised.
The expression evaluator currently doesn't account for differing
string collations between the database and Python.
:param synchronize_session: chooses the strategy to update the
attributes on objects in the session. See the section
:ref:`bulk_update_delete` for a discussion of these strategies.
:return: the count of rows matched as returned by the database's
"row count" feature.
.. warning:: **Additional Caveats for bulk query deletes**
* This method does **not work for joined
inheritance mappings**, since the **multiple table
deletes are not supported by SQL** as well as that the
**join condition of an inheritance mapper is not
automatically rendered**. Care must be taken in any
multiple-table delete to first accommodate via some other means
how the related table will be deleted, as well as to
explicitly include the joining
condition between those tables, even in mappings where
this is normally automatic. E.g. if a class ``Engineer``
subclasses ``Employee``, a DELETE against the ``Employee``
table would look like::
session.query(Engineer).\
filter(Engineer.id == Employee.id).\
filter(Employee.name == 'dilbert').\
delete()
However the above SQL will not delete from the Engineer table,
unless an ON DELETE CASCADE rule is established in the database
to handle it.
Short story, **do not use this method for joined inheritance
mappings unless you have taken the additional steps to make
this feasible**.
* The polymorphic identity WHERE criteria is **not** included
for single- or
joined- table updates - this must be added **manually** even
for single table inheritance.
* The method does **not** offer in-Python cascading of
relationships - it is assumed that ON DELETE CASCADE/SET
NULL/etc. is configured for any foreign key references
which require it, otherwise the database may emit an
integrity violation if foreign key references are being
enforced.
After the DELETE, dependent objects in the
:class:`.Session` which were impacted by an ON DELETE
may not contain the current state, or may have been
deleted. This issue is resolved once the
:class:`.Session` is expired, which normally occurs upon
:meth:`.Session.commit` or can be forced by using
:meth:`.Session.expire_all`. Accessing an expired
object whose row has been deleted will invoke a SELECT
to locate the row; when the row is not found, an
:class:`~sqlalchemy.orm.exc.ObjectDeletedError` is
raised.
* The ``'fetch'`` strategy results in an additional
SELECT statement emitted and will significantly reduce
performance.
* The ``'evaluate'`` strategy performs a scan of
all matching objects within the :class:`.Session`; if the
contents of the :class:`.Session` are expired, such as
via a proceeding :meth:`.Session.commit` call, **this will
result in SELECT queries emitted for every matching object**.
* The :meth:`.MapperEvents.before_delete` and
:meth:`.MapperEvents.after_delete`
events **are not invoked** from this method. Instead, the
:meth:`.SessionEvents.after_bulk_delete` method is provided to
act upon a mass DELETE of entity rows.
.. seealso::
:meth:`_query.Query.update`
:ref:`inserts_and_updates` - Core SQL tutorial
:ref:`bulk_update_delete`
"""
@@ -3231,12 +3004,11 @@ class Query(
sess.query(User).filter(User.age == 25).\
update({"age": User.age - 10}, synchronize_session='evaluate')
.. warning::
.. warning:: The :meth:`_query.Query.update`
method is a "bulk" operation,
which bypasses ORM unit-of-work automation in favor of greater
performance. **Please read all caveats and warnings below.**
See the section :ref:`bulk_update_delete` for important caveats
and warnings, including limitations when using bulk UPDATE
and DELETE with mapper inheritance configurations.
:param values: a dictionary with attributes names, or alternatively
mapped attributes or SQL expressions, as keys, and literal
@@ -3248,31 +3020,9 @@ class Query(
flag is passed to the :paramref:`.Query.update.update_args` dictionary
as well.
.. versionchanged:: 1.0.0 - string names in the values dictionary
are now resolved against the mapped entity; previously, these
strings were passed as literal column names with no mapper-level
translation.
:param synchronize_session: chooses the strategy to update the
attributes on objects in the session. Valid values are:
``False`` - don't synchronize the session. This option is the most
efficient and is reliable once the session is expired, which
typically occurs after a commit(), or explicitly using
expire_all(). Before the expiration, updated objects may still
remain in the session with stale values on their attributes, which
can lead to confusing results.
``'fetch'`` - performs a select query before the update to find
objects that are matched by the update query. The updated
attributes are expired on matched objects.
``'evaluate'`` - Evaluate the Query's criteria in Python straight
on the objects in the session. If evaluation of the criteria isn't
implemented, an exception is raised.
The expression evaluator currently doesn't account for differing
string collations between the database and Python.
attributes on objects in the session. See the section
:ref:`bulk_update_delete` for a discussion of these strategies.
:param update_args: Optional dictionary, if present will be passed
to the underlying :func:`_expression.update`
@@ -3281,70 +3031,14 @@ class Query(
as ``mysql_limit``, as well as other special arguments such as
:paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order`.
.. versionadded:: 1.0.0
:return: the count of rows matched as returned by the database's
"row count" feature.
.. warning:: **Additional Caveats for bulk query updates**
* The method does **not** offer in-Python cascading of
relationships - it is assumed that ON UPDATE CASCADE is
configured for any foreign key references which require
it, otherwise the database may emit an integrity
violation if foreign key references are being enforced.
After the UPDATE, dependent objects in the
:class:`.Session` which were impacted by an ON UPDATE
CASCADE may not contain the current state; this issue is
resolved once the :class:`.Session` is expired, which
normally occurs upon :meth:`.Session.commit` or can be
forced by using :meth:`.Session.expire_all`.
* The ``'fetch'`` strategy results in an additional
SELECT statement emitted and will significantly reduce
performance.
* The ``'evaluate'`` strategy performs a scan of
all matching objects within the :class:`.Session`; if the
contents of the :class:`.Session` are expired, such as
via a proceeding :meth:`.Session.commit` call, **this will
result in SELECT queries emitted for every matching object**.
* The method supports multiple table updates, as detailed
in :ref:`multi_table_updates`, and this behavior does
extend to support updates of joined-inheritance and
other multiple table mappings. However, the **join
condition of an inheritance mapper is not
automatically rendered**. Care must be taken in any
multiple-table update to explicitly include the joining
condition between those tables, even in mappings where
this is normally automatic. E.g. if a class ``Engineer``
subclasses ``Employee``, an UPDATE of the ``Engineer``
local table using criteria against the ``Employee``
local table might look like::
session.query(Engineer).\
filter(Engineer.id == Employee.id).\
filter(Employee.name == 'dilbert').\
update({"engineer_type": "programmer"})
* The polymorphic identity WHERE criteria is **not** included
for single- or
joined- table updates - this must be added **manually**, even
for single table inheritance.
* The :meth:`.MapperEvents.before_update` and
:meth:`.MapperEvents.after_update`
events **are not invoked from this method**. Instead, the
:meth:`.SessionEvents.after_bulk_update` method is provided to
act upon a mass UPDATE of entity rows.
.. seealso::
:meth:`_query.Query.delete`
:ref:`bulk_update_delete`
:ref:`inserts_and_updates` - Core SQL tutorial
"""
@@ -3390,7 +3084,7 @@ class Query(
"""
stmt = self._statement_20(for_statement=for_statement, **kw)
assert for_statement == stmt.compile_options._for_statement
assert for_statement == stmt._compile_options._for_statement
# this chooses between ORMFromStatementCompileState and
# ORMSelectCompileState. We could also base this on
@@ -3422,7 +3116,7 @@ class FromStatement(SelectStatementGrouping, Executable):
__visit_name__ = "orm_from_statement"
compile_options = ORMFromStatementCompileState.default_compile_options
_compile_options = ORMFromStatementCompileState.default_compile_options
_compile_state_factory = ORMFromStatementCompileState.create_for_statement
+485 -228
View File
@@ -30,6 +30,7 @@ from .unitofwork import UOWTransaction
from .. import engine
from .. import exc as sa_exc
from .. import future
from .. import sql
from .. import util
from ..inspection import inspect
from ..sql import coercions
@@ -288,7 +289,7 @@ class ORMExecuteState(util.MemoizedSlots):
return self.statement._execution_options.union(self._execution_options)
def _orm_compile_options(self):
opts = self.statement.compile_options
opts = self.statement._compile_options
if isinstance(opts, context.ORMCompileState.default_compile_options):
return opts
else:
@@ -367,134 +368,53 @@ class ORMExecuteState(util.MemoizedSlots):
class SessionTransaction(object):
"""A :class:`.Session`-level transaction.
:class:`.SessionTransaction` is a mostly behind-the-scenes object
not normally referenced directly by application code. It coordinates
among multiple :class:`_engine.Connection` objects, maintaining a database
transaction for each one individually, committing or rolling them
back all at once. It also provides optional two-phase commit behavior
which can augment this coordination operation.
:class:`.SessionTransaction` is produced from the
:meth:`_orm.Session.begin`
and :meth:`_orm.Session.begin_nested` methods. It's largely an internal
object that in modern use provides a context manager for session
transactions.
The :attr:`.Session.transaction` attribute of :class:`.Session`
refers to the current :class:`.SessionTransaction` object in use, if any.
The :attr:`.SessionTransaction.parent` attribute refers to the parent
:class:`.SessionTransaction` in the stack of :class:`.SessionTransaction`
objects. If this attribute is ``None``, then this is the top of the stack.
If non-``None``, then this :class:`.SessionTransaction` refers either
to a so-called "subtransaction" or a "nested" transaction. A
"subtransaction" is a scoping concept that demarcates an inner portion
of the outermost "real" transaction. A nested transaction, which
is indicated when the :attr:`.SessionTransaction.nested`
attribute is also True, indicates that this :class:`.SessionTransaction`
corresponds to a SAVEPOINT.
Documentation on interacting with :class:`_orm.SessionTransaction` is
at: :ref:`unitofwork_transaction`.
**Life Cycle**
A :class:`.SessionTransaction` is associated with a :class:`.Session` in
its default mode of ``autocommit=False`` whenever the "autobegin" process
takes place, associated with no database connections. As the
:class:`.Session` is called upon to emit SQL on behalf of various
:class:`_engine.Engine` or :class:`_engine.Connection` objects,
a corresponding
:class:`_engine.Connection` and associated :class:`.Transaction`
is added to a
collection within the :class:`.SessionTransaction` object, becoming one of
the connection/transaction pairs maintained by the
:class:`.SessionTransaction`. The start of a :class:`.SessionTransaction`
can be tracked using the :meth:`.SessionEvents.after_transaction_create`
event.
The lifespan of the :class:`.SessionTransaction` ends when the
:meth:`.Session.commit`, :meth:`.Session.rollback` or
:meth:`.Session.close` methods are called. At this point, the
:class:`.SessionTransaction` removes its association with its parent
:class:`.Session`. A :class:`.Session` that is in ``autocommit=False``
mode will create a new :class:`.SessionTransaction` to replace it when the
next "autobegin" event occurs, whereas a :class:`.Session` that's in
``autocommit=True`` mode will remain without a :class:`.SessionTransaction`
until the :meth:`.Session.begin` method is called. The end of a
:class:`.SessionTransaction` can be tracked using the
:meth:`.SessionEvents.after_transaction_end` event.
.. versionchanged:: 1.4 the :class:`.SessionTransaction` is not created
immediately within a :class:`.Session` when constructed or when the
previous transaction is removed, it instead is created when the
:class:`.Session` is next used.
**Nesting and Subtransactions**
Another detail of :class:`.SessionTransaction` behavior is that it is
capable of "nesting". This means that the :meth:`.Session.begin` method
can be called while an existing :class:`.SessionTransaction` is already
present, producing a new :class:`.SessionTransaction` that temporarily
replaces the parent :class:`.SessionTransaction`. When a
:class:`.SessionTransaction` is produced as nested, it assigns itself to
the :attr:`.Session.transaction` attribute, and it additionally will assign
the previous :class:`.SessionTransaction` to its :attr:`.Session.parent`
attribute. The behavior is effectively a
stack, where :attr:`.Session.transaction` refers to the current head of
the stack, and the :attr:`.SessionTransaction.parent` attribute allows
traversal up the stack until :attr:`.SessionTransaction.parent` is
``None``, indicating the top of the stack.
When the scope of :class:`.SessionTransaction` is ended via
:meth:`.Session.commit` or :meth:`.Session.rollback`, it restores its
parent :class:`.SessionTransaction` back onto the
:attr:`.Session.transaction` attribute.
The purpose of this stack is to allow nesting of
:meth:`.Session.rollback` or :meth:`.Session.commit` calls in context
with various flavors of :meth:`.Session.begin`. This nesting behavior
applies to when :meth:`.Session.begin_nested` is used to emit a
SAVEPOINT transaction, and is also used to produce a so-called
"subtransaction" which allows a block of code to use a
begin/rollback/commit sequence regardless of whether or not its enclosing
code block has begun a transaction. The :meth:`.flush` method, whether
called explicitly or via autoflush, is the primary consumer of the
"subtransaction" feature, in that it wishes to guarantee that it works
within in a transaction block regardless of whether or not the
:class:`.Session` is in transactional mode when the method is called.
Note that the flush process that occurs within the "autoflush" feature
as well as when the :meth:`.Session.flush` method is used **always**
creates a :class:`.SessionTransaction` object. This object is normally
a subtransaction, unless the :class:`.Session` is in autocommit mode
and no transaction exists at all, in which case it's the outermost
transaction. Any event-handling logic or other inspection logic
needs to take into account whether a :class:`.SessionTransaction`
is the outermost transaction, a subtransaction, or a "nested" / SAVEPOINT
transaction.
.. versionchanged:: 1.4 The scoping and API methods to work with the
:class:`_orm.SessionTransaction` object directly have been simplified.
.. seealso::
:meth:`.Session.rollback`
:meth:`.Session.commit`
:ref:`unitofwork_transaction`
:meth:`.Session.begin`
:meth:`.Session.begin_nested`
:attr:`.Session.is_active`
:meth:`.Session.rollback`
:meth:`.SessionEvents.after_transaction_create`
:meth:`.Session.commit`
:meth:`.SessionEvents.after_transaction_end`
:meth:`.Session.in_transaction`
:meth:`.SessionEvents.after_commit`
:meth:`.Session.in_nested_transaction`
:meth:`.SessionEvents.after_rollback`
:meth:`.Session.get_transaction`
:meth:`.Session.get_nested_transaction`
:meth:`.SessionEvents.after_soft_rollback`
"""
_rollback_exception = None
def __init__(self, session, parent=None, nested=False, autobegin=False):
def __init__(
self, session, parent=None, nested=False, autobegin=False,
):
self.session = session
self._connections = {}
self._parent = parent
self.nested = nested
if nested:
self._previous_nested_transaction = session._nested_transaction
self._state = ACTIVE
if not parent and nested:
raise sa_exc.InvalidRequestError(
@@ -688,6 +608,8 @@ class SessionTransaction(object):
return self._connections[bind][0]
local_connect = False
should_commit = True
if self._parent:
conn = self._parent._connection_for_bind(bind, execution_options)
if not self.nested:
@@ -712,11 +634,16 @@ class SessionTransaction(object):
transaction = conn.begin_twophase()
elif self.nested:
transaction = conn.begin_nested()
else:
if conn._is_future and conn.in_transaction():
transaction = conn._transaction
elif conn.in_transaction():
# if given a future connection already in a transaction, don't
# commit that transaction unless it is a savepoint
if conn.in_nested_transaction():
transaction = conn.get_nested_transaction()
else:
transaction = conn.begin()
transaction = conn.get_transaction()
should_commit = False
else:
transaction = conn.begin()
except:
# connection will not not be associated with this Session;
# close it immediately so that it isn't closed under GC
@@ -729,7 +656,7 @@ class SessionTransaction(object):
self._connections[conn] = self._connections[conn.engine] = (
conn,
transaction,
not bind_is_connection or not conn._is_future,
should_commit,
not bind_is_connection,
)
self.session.dispatch.after_begin(self.session, self, conn)
@@ -748,7 +675,7 @@ class SessionTransaction(object):
if self._parent is None or self.nested:
self.session.dispatch.before_commit(self.session)
stx = self.session.transaction
stx = self.session._transaction
if stx is not self:
for subtransaction in stx._iterate_self_and_parents(upto=self):
subtransaction.commit()
@@ -775,7 +702,7 @@ class SessionTransaction(object):
self._state = PREPARED
def commit(self):
def commit(self, _to_root=False):
self._assert_active(prepared_ok=True)
if self._state is not PREPARED:
self._prepare_impl()
@@ -793,12 +720,16 @@ class SessionTransaction(object):
self._remove_snapshot()
self.close()
if _to_root and self._parent:
return self._parent.commit(_to_root=True)
return self._parent
def rollback(self, _capture_exception=False):
def rollback(self, _capture_exception=False, _to_root=False):
self._assert_active(prepared_ok=True, rollback_ok=True)
stx = self.session.transaction
stx = self.session._transaction
if stx is not self:
for subtransaction in stx._iterate_self_and_parents(upto=self):
subtransaction.close()
@@ -849,20 +780,28 @@ class SessionTransaction(object):
sess.dispatch.after_soft_rollback(sess, self)
if _to_root and self._parent:
return self._parent.rollback(_to_root=True)
return self._parent
def close(self, invalidate=False):
if self.nested:
self.session._nested_transaction = (
self._previous_nested_transaction
)
self.session._transaction = self._parent
if self._parent is None:
for connection, transaction, should_commit, autoclose in set(
self._connections.values()
):
if invalidate:
connection.invalidate()
if should_commit and transaction.is_active:
transaction.close()
if autoclose:
connection.close()
else:
transaction.close()
self._state = CLOSED
self.session.dispatch.after_transaction_end(self.session, self)
@@ -924,6 +863,15 @@ class Session(_SessionClassMethods):
"scalar",
)
@util.deprecated_params(
autocommit=(
"2.0",
"The :paramref:`.Session.autocommit` parameter is deprecated "
"and will be removed in SQLAlchemy version 2.0. Please use the "
":paramref:`.Session.autobegin` parameter set to False to support "
"explicit use of the :meth:`.Session.begin` method.",
),
)
def __init__(
self,
bind=None,
@@ -1071,8 +1019,6 @@ class Session(_SessionClassMethods):
:class:`.Session` dictionary will be local to that
:class:`.Session`.
.. versionadded:: 0.9.0
:param query_cls: Class which should be used to create new Query
objects, as returned by the :meth:`~.Session.query` method.
Defaults to :class:`_query.Query`.
@@ -1096,13 +1042,23 @@ class Session(_SessionClassMethods):
self._flushing = False
self._warn_on_events = False
self._transaction = None
self._nested_transaction = None
self.future = future
self.hash_key = _new_sessionid()
self.autoflush = autoflush
self.autocommit = autocommit
self.expire_on_commit = expire_on_commit
self.enable_baked_queries = enable_baked_queries
if autocommit:
if future:
raise sa_exc.ArgumentError(
"Cannot use autocommit mode with future=True. "
"use the autobegin flag."
)
self.autocommit = True
else:
self.autocommit = False
self.twophase = twophase
self._query_cls = query_cls if query_cls else query.Query
if info:
@@ -1116,21 +1072,77 @@ class Session(_SessionClassMethods):
connection_callable = None
def __enter__(self):
return self
def __exit__(self, type_, value, traceback):
self.close()
@property
@util.deprecated_20(
"The :attr:`_orm.Session.transaction` accessor is deprecated and "
"will be removed in SQLAlchemy version 2.0. "
"For context manager use, use :meth:`_orm.Session.begin`. To access "
"the current root transaction, use "
":meth:`_orm.Session.get_transaction()"
)
def transaction(self):
"""The current active or inactive :class:`.SessionTransaction`.
If this session is in "autobegin" mode and the transaction was not
begun, this accessor will implicitly begin the transaction.
May be None if no transaction has begun yet.
.. versionchanged:: 1.4 the :attr:`.Session.transaction` attribute
is now a read-only descriptor that will automatically start a
transaction in "autobegin" mode if one is not present.
is now a read-only descriptor that also may return None if no
transaction has begun yet.
"""
self._autobegin()
if not self.future:
self._autobegin()
return self._transaction
def in_transaction(self):
"""Return True if this :class:`_orm.Session` has begun a transaction.
.. versionadded:: 1.4
.. seealso::
:attr:`_orm.Session.is_active`
"""
return self._transaction is not None
def in_nested_transaction(self):
"""Return True if this :class:`_orm.Session` has begun a nested
transaction, e.g. SAVEPOINT.
.. versionadded:: 1.4
"""
return self._nested_transaction is not None
def get_transaction(self):
"""Return the current root transaction in progress, if any.
.. versionadded:: 1.4
"""
trans = self._transaction
while trans is not None and trans._parent is not None:
trans = trans._parent
return trans
def get_nested_transaction(self):
"""Return the current nested transaction in progress, if any.
.. versionadded:: 1.4
"""
return self._nested_transaction
@util.memoized_property
def info(self):
"""A user-modifiable dictionary.
@@ -1141,8 +1153,6 @@ class Session(_SessionClassMethods):
here is always local to this :class:`.Session` and can be modified
independently of all other :class:`.Session` objects.
.. versionadded:: 0.9.0
"""
return {}
@@ -1153,7 +1163,17 @@ class Session(_SessionClassMethods):
return False
def begin(self, subtransactions=False, nested=False):
@util.deprecated_params(
subtransactions=(
"2.0",
"The :paramref:`_orm.Session.begin.subtransactions` flag is "
"deprecated and "
"will be removed in SQLAlchemy version 2.0. The "
":attr:`_orm.Session.transaction` flag may "
"be checked for None before invoking :meth:`_orm.Session.begin`.",
)
)
def begin(self, subtransactions=False, nested=False, _subtrans=False):
"""Begin a transaction on this :class:`.Session`.
.. warning::
@@ -1206,17 +1226,24 @@ class Session(_SessionClassMethods):
"""
if subtransactions and self.future:
raise NotImplementedError(
"subtransactions are not implemented in future "
"Session objects."
)
if self._autobegin():
if not subtransactions and not nested:
return
return self._transaction
if self._transaction is not None:
if subtransactions or nested:
self._transaction = self._transaction._begin(nested=nested)
if subtransactions or _subtrans or nested:
trans = self._transaction._begin(nested=nested)
self._transaction = trans
if nested:
self._nested_transaction = trans
else:
raise sa_exc.InvalidRequestError(
"A transaction is already begun. Use "
"subtransactions=True to allow subtransactions."
"A transaction is already begun on this Session."
)
else:
self._transaction = SessionTransaction(self, nested=nested)
@@ -1265,7 +1292,7 @@ class Session(_SessionClassMethods):
if self._transaction is None:
pass
else:
self._transaction.rollback()
self._transaction.rollback(_to_root=self.future)
def commit(self):
"""Flush pending changes and commit the current transaction.
@@ -1299,7 +1326,7 @@ class Session(_SessionClassMethods):
if not self._autobegin():
raise sa_exc.InvalidRequestError("No transaction is begun.")
self._transaction.commit()
self._transaction.commit(_to_root=self.future)
def prepare(self):
"""Prepare the current transaction in progress for two phase commit.
@@ -1371,8 +1398,6 @@ class Session(_SessionClassMethods):
present within the :class:`.Session`, a warning is emitted and
the arguments are ignored.
.. versionadded:: 0.9.9
.. seealso::
:ref:`session_transaction_isolation`
@@ -1402,6 +1427,7 @@ class Session(_SessionClassMethods):
)
assert self._transaction is None
assert self.autocommit
conn = engine.connect(**kw)
if execution_options:
conn = conn.execution_options(**execution_options)
@@ -1663,12 +1689,19 @@ class Session(_SessionClassMethods):
This is a variant of :meth:`.Session.close` that will additionally
ensure that the :meth:`_engine.Connection.invalidate`
method will be called
on all :class:`_engine.Connection` objects. This can be called when
the database is known to be in a state where the connections are
no longer safe to be used.
method will be called on each :class:`_engine.Connection` object
that is currently in use for a transaction (typically there is only
one connection unless the :class:`_orm.Session` is used with
multiple engines).
E.g.::
This can be called when the database is known to be in a state where
the connections are no longer safe to be used.
Below illustrates a scenario when using `gevent
<http://www.gevent.org/>`_, which can produce ``Timeout`` exceptions
that may mean the underlying connection should be discarded::
import gevent
try:
sess = Session()
@@ -1681,13 +1714,8 @@ class Session(_SessionClassMethods):
sess.rollback()
raise
This clears all items and ends any transaction in progress.
If this session were created with ``autocommit=False``, a new
transaction is immediately begun. Note that this new transaction does
not use any connection resources until they are first needed.
.. versionadded:: 0.9.9
The method additionally does everything that :meth:`_orm.Session.close`
does, including that all ORM objects are expunged.
"""
self._close_impl(invalidate=True)
@@ -2118,13 +2146,7 @@ class Session(_SessionClassMethods):
"A blank dictionary is ambiguous."
)
if with_for_update is not None:
if with_for_update is True:
with_for_update = query.ForUpdateArg()
elif with_for_update:
with_for_update = query.ForUpdateArg(**with_for_update)
else:
with_for_update = None
with_for_update = query.ForUpdateArg._from_argument(with_for_update)
stmt = future.select(object_mapper(instance))
if (
@@ -2482,6 +2504,200 @@ class Session(_SessionClassMethods):
for o, m, st_, dct_ in cascade_states:
self._delete_impl(st_, o, False)
def get(
self,
entity,
ident,
options=None,
populate_existing=False,
with_for_update=None,
identity_token=None,
):
"""Return an instance based on the given primary key identifier,
or ``None`` if not found.
E.g.::
my_user = session.get(User, 5)
some_object = session.get(VersionedFoo, (5, 10))
some_object = session.get(
VersionedFoo,
{"id": 5, "version_id": 10}
)
.. versionadded:: 1.4 Added :meth:`_orm.Session.get`, which is moved
from the now deprecated :meth:`_orm.Query.get` method.
:meth:`_orm.Session.get` is special in that it provides direct
access to the identity map of the :class:`.Session`.
If the given primary key identifier is present
in the local identity map, the object is returned
directly from this collection and no SQL is emitted,
unless the object has been marked fully expired.
If not present,
a SELECT is performed in order to locate the object.
:meth:`_orm.Session.get` also will perform a check if
the object is present in the identity map and
marked as expired - a SELECT
is emitted to refresh the object as well as to
ensure that the row is still present.
If not, :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised.
:param entity: a mapped class or :class:`.Mapper` indicating the
type of entity to be loaded.
:param ident: A scalar, tuple, or dictionary representing the
primary key. For a composite (e.g. multiple column) primary key,
a tuple or dictionary should be passed.
For a single-column primary key, the scalar calling form is typically
the most expedient. If the primary key of a row is the value "5",
the call looks like::
my_object = session.get(SomeClass, 5)
The tuple form contains primary key values typically in
the order in which they correspond to the mapped
:class:`_schema.Table`
object's primary key columns, or if the
:paramref:`_orm.Mapper.primary_key` configuration parameter were
used, in
the order used for that parameter. For example, if the primary key
of a row is represented by the integer
digits "5, 10" the call would look like::
my_object = session.get(SomeClass, (5, 10))
The dictionary form should include as keys the mapped attribute names
corresponding to each element of the primary key. If the mapped class
has the attributes ``id``, ``version_id`` as the attributes which
store the object's primary key value, the call would look like::
my_object = session.get(SomeClass, {"id": 5, "version_id": 10})
:param options: optional sequence of loader options which will be
applied to the query, if one is emitted.
:param populate_existing: causes the method to unconditionally emit
a SQL query and refresh the object with the newly loaded data,
regardless of whether or not the object is already present.
:param with_for_update: optional boolean ``True`` indicating FOR UPDATE
should be used, or may be a dictionary containing flags to
indicate a more specific set of FOR UPDATE flags for the SELECT;
flags should match the parameters of
:meth:`_query.Query.with_for_update`.
Supersedes the :paramref:`.Session.refresh.lockmode` parameter.
:return: The object instance, or ``None``.
"""
return self._get_impl(
entity,
ident,
loading.load_on_pk_identity,
options,
populate_existing=populate_existing,
with_for_update=with_for_update,
identity_token=identity_token,
)
def _get_impl(
self,
entity,
primary_key_identity,
db_load_fn,
options=None,
populate_existing=False,
with_for_update=None,
identity_token=None,
execution_options=None,
):
# convert composite types to individual args
if hasattr(primary_key_identity, "__composite_values__"):
primary_key_identity = primary_key_identity.__composite_values__()
mapper = inspect(entity)
is_dict = isinstance(primary_key_identity, dict)
if not is_dict:
primary_key_identity = util.to_list(
primary_key_identity, default=(None,)
)
if len(primary_key_identity) != len(mapper.primary_key):
raise sa_exc.InvalidRequestError(
"Incorrect number of values in identifier to formulate "
"primary key for query.get(); primary key columns are %s"
% ",".join("'%s'" % c for c in mapper.primary_key)
)
if is_dict:
try:
primary_key_identity = list(
primary_key_identity[prop.key]
for prop in mapper._identity_key_props
)
except KeyError as err:
util.raise_(
sa_exc.InvalidRequestError(
"Incorrect names of values in identifier to formulate "
"primary key for query.get(); primary key attribute "
"names are %s"
% ",".join(
"'%s'" % prop.key
for prop in mapper._identity_key_props
)
),
replace_context=err,
)
if (
not populate_existing
and not mapper.always_refresh
and with_for_update is None
):
instance = self._identity_lookup(
mapper, primary_key_identity, identity_token=identity_token
)
if instance is not None:
# reject calls for id in identity map but class
# mismatch.
if not issubclass(instance.__class__, mapper.class_):
return None
return instance
elif instance is attributes.PASSIVE_CLASS_MISMATCH:
return None
# apply_labels() not strictly necessary, however this will ensure that
# tablename_colname style is used which at the moment is asserted
# in a lot of unit tests :)
load_options = context.QueryContext.default_load_options
if populate_existing:
load_options += {"_populate_existing": populate_existing}
statement = sql.select(mapper).apply_labels()
if with_for_update is not None:
statement._for_update_arg = query.ForUpdateArg._from_argument(
with_for_update
)
if options:
statement = statement.options(*options)
if execution_options:
statement = statement.execution_options(**execution_options)
return db_load_fn(
self, statement, primary_key_identity, load_options=load_options,
)
def merge(self, instance, load=True):
"""Copy the state of a given instance into a corresponding instance
within this :class:`.Session`.
@@ -2629,7 +2845,7 @@ class Session(_SessionClassMethods):
new_instance = True
elif key_is_persistent:
merged = self.query(mapper.class_).get(key[1])
merged = self.get(mapper.class_, key[1], identity_token=key[2])
if merged is None:
merged = mapper.class_manager.new_instance()
@@ -3021,9 +3237,7 @@ class Session(_SessionClassMethods):
if not flush_context.has_work:
return
flush_context.transaction = transaction = self.begin(
subtransactions=True
)
flush_context.transaction = transaction = self.begin(_subtrans=True)
try:
self._warn_on_events = True
try:
@@ -3338,7 +3552,7 @@ class Session(_SessionClassMethods):
mapper = _class_to_mapper(mapper)
self._flushing = True
transaction = self.begin(subtransactions=True)
transaction = self.begin(_subtrans=True)
try:
if isupdate:
persistence._bulk_update(
@@ -3441,62 +3655,38 @@ class Session(_SessionClassMethods):
@property
def is_active(self):
"""True if this :class:`.Session` is in "transaction mode" and
is not in "partial rollback" state.
"""True if this :class:`.Session` not in "partial rollback" state.
The :class:`.Session` in its default mode of ``autocommit=False``
is essentially always in "transaction mode", in that a
:class:`.SessionTransaction` is associated with it as soon as
it is instantiated. This :class:`.SessionTransaction` is immediately
replaced with a new one as soon as it is ended, due to a rollback,
commit, or close operation.
.. versionchanged:: 1.4 The :class:`_orm.Session` no longer begins
a new transaction immediately, so this attribute will be False
when the :class:`_orm.Session` is first instantiated.
"Transaction mode" does *not* indicate whether
or not actual database connection resources are in use; the
:class:`.SessionTransaction` object coordinates among zero or more
actual database transactions, and starts out with none, accumulating
individual DBAPI connections as different data sources are used
within its scope. The best way to track when a particular
:class:`.Session` has actually begun to use DBAPI resources is to
implement a listener using the :meth:`.SessionEvents.after_begin`
method, which will deliver both the :class:`.Session` as well as the
target :class:`_engine.Connection` to a user-defined event listener.
"partial rollback" state typically indicates that the flush process
of the :class:`_orm.Session` has failed, and that the
:meth:`_orm.Session.rollback` method must be emitted in order to
fully roll back the transaction.
The "partial rollback" state refers to when an "inner" transaction,
typically used during a flush, encounters an error and emits a
rollback of the DBAPI connection. At this point, the
:class:`.Session` is in "partial rollback" and awaits for the user to
call :meth:`.Session.rollback`, in order to close out the
transaction stack. It is in this "partial rollback" period that the
:attr:`.is_active` flag returns False. After the call to
:meth:`.Session.rollback`, the :class:`.SessionTransaction` is
replaced with a new one and :attr:`.is_active` returns ``True`` again.
If this :class:`_orm.Session` is not in a transaction at all, the
:class:`_orm.Session` will autobegin when it is first used, so in this
case :attr:`_orm.Session.is_active` will return True.
When a :class:`.Session` is used in ``autocommit=True`` mode, the
:class:`.SessionTransaction` is only instantiated within the scope
of a flush call, or when :meth:`.Session.begin` is called. So
:attr:`.is_active` will always be ``False`` outside of a flush or
:meth:`.Session.begin` block in this mode, and will be ``True``
within the :meth:`.Session.begin` block as long as it doesn't enter
"partial rollback" state.
Otherwise, if this :class:`_orm.Session` is within a transaction,
and that transaction has not been rolled back internally, the
:attr:`_orm.Session.is_active` will also return True.
From all the above, it follows that the only purpose to this flag is
for application frameworks that wish to detect if a "rollback" is
necessary within a generic error handling routine, for
:class:`.Session` objects that would otherwise be in
"partial rollback" mode. In a typical integration case, this is also
not necessary as it is standard practice to emit
:meth:`.Session.rollback` unconditionally within the outermost
exception catch.
.. seealso::
To track the transactional state of a :class:`.Session` fully,
use event listeners, primarily the :meth:`.SessionEvents.after_begin`,
:meth:`.SessionEvents.after_commit`,
:meth:`.SessionEvents.after_rollback` and related events.
:ref:`faq_session_rollback`
:meth:`_orm.Session.in_transaction`
"""
self._autobegin()
return self._transaction and self._transaction.is_active
if self.autocommit:
return (
self._transaction is not None and self._transaction.is_active
)
else:
return self._transaction is None or self._transaction.is_active
identity_map = None
"""A mapping of object identities to objects themselves.
@@ -3576,36 +3766,84 @@ class sessionmaker(_SessionClassMethods):
e.g.::
# global scope
Session = sessionmaker(autoflush=False)
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# later, in a local scope, create and use a session:
sess = Session()
# an Engine, which the Session will use for connection
# resources
engine = create_engine('postgresql://scott:tiger@localhost/')
Any keyword arguments sent to the constructor itself will override the
"configured" keywords::
Session = sessionmaker(engine)
Session = sessionmaker()
with Session() as session:
session.add(some_object)
session.add(some_other_object)
session.commit()
Context manager use is optional; otherwise, the returned
:class:`_orm.Session` object may be closed explicitly via the
:meth:`_orm.Session.close` method. Using a
``try:/finally:`` block is optional, however will ensure that the close
takes place even if there are database errors::
session = Session()
try:
session.add(some_object)
session.add(some_other_object)
session.commit()
finally:
session.close()
:class:`.sessionmaker` acts as a factory for :class:`_orm.Session`
objects in the same way as an :class:`_engine.Engine` acts as a factory
for :class:`_engine.Connection` objects. In this way it also includes
a :meth:`_orm.sessionmaker.begin` method, that provides a context
manager which both begins and commits a transaction, as well as closes
out the :class:`_orm.Session` when complete, rolling back the transaction
if any errors occur::
Session = sessionmaker(engine)
wih Session.begin() as session:
session.add(some_object)
session.add(some_other_object)
# commits transaction, closes session
.. versionadded:: 1.4
When calling upon :class:`_orm.sessionmaker` to construct a
:class:`_orm.Session`, keyword arguments may also be passed to the
method; these arguments will override that of the globally configured
parameters. Below we use a :class:`_orm.sessionmaker` bound to a certain
:class:`_engine.Engine` to produce a :class:`_orm.Session` that is instead
bound to a specific :class:`_engine.Connection` procured from that engine::
Session = sessionmaker(engine)
# bind an individual session to a connection
sess = Session(bind=connection)
The class also includes a method :meth:`.configure`, which can
be used to specify additional keyword arguments to the factory, which
will take effect for subsequent :class:`.Session` objects generated.
This is usually used to associate one or more :class:`_engine.Engine`
objects
with an existing :class:`.sessionmaker` factory before it is first
used::
with engine.connect() as connection:
with Session(bind=connection) as session:
# work with session
# application starts
The class also includes a method :meth:`_orm.sessionmaker.configure`, which
can be used to specify additional keyword arguments to the factory, which
will take effect for subsequent :class:`.Session` objects generated. This
is usually used to associate one or more :class:`_engine.Engine` objects
with an existing
:class:`.sessionmaker` factory before it is first used::
# application starts, sessionmaker does not have
# an engine bound yet
Session = sessionmaker()
# ... later
# ... later, when an engine URL is read from a configuration
# file or other events allow the engine to be created
engine = create_engine('sqlite:///foo.db')
Session.configure(bind=engine)
sess = Session()
# work with session
.. seealso::
@@ -3646,8 +3884,6 @@ class sessionmaker(_SessionClassMethods):
replaced, when the ``info`` parameter is specified to the specific
:class:`.Session` construction operation.
.. versionadded:: 0.9.0
:param \**kw: all other keyword arguments are passed to the
constructor of newly created :class:`.Session` objects.
@@ -3663,6 +3899,29 @@ class sessionmaker(_SessionClassMethods):
# events can be associated with it specifically.
self.class_ = type(class_.__name__, (class_,), {})
@util.contextmanager
def begin(self):
"""Produce a context manager that both provides a new
:class:`_orm.Session` as well as a transaction that commits.
e.g.::
Session = sessionmaker(some_engine)
with Session.begin() as session:
session.add(some_object)
# commits transaction, closes session
.. versionadded:: 1.4
"""
with self() as session:
with session.begin():
yield session
def __call__(self, **local_kw):
"""Produce a new :class:`.Session` object using the configuration
established in this :class:`.sessionmaker`.
@@ -3806,8 +4065,6 @@ def make_transient_to_detached(instance):
call to :meth:`.Session.merge` in that a given persistent state
can be manufactured without any SQL calls.
.. versionadded:: 0.9.5
.. seealso::
:func:`.make_transient`
+1 -1
View File
@@ -677,7 +677,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
self._equated_columns[c] = self._equated_columns[col]
self.logger.info(
"%s will use query.get() to " "optimize instance loads", self
"%s will use Session.get() to " "optimize instance loads", self
)
def init_class_attribute(self, mapper):
+105
View File
@@ -1621,3 +1621,108 @@ def randomize_unitofwork():
topological.set = (
unitofwork.set
) = session.set = mapper.set = dependency.set = RandomSet
def _offset_or_limit_clause(element, name=None, type_=None):
"""Convert the given value to an "offset or limit" clause.
This handles incoming integers and converts to an expression; if
an expression is already given, it is passed through.
"""
return coercions.expect(
roles.LimitOffsetRole, element, name=name, type_=type_
)
def _offset_or_limit_clause_asint_if_possible(clause):
"""Return the offset or limit clause as a simple integer if possible,
else return the clause.
"""
if clause is None:
return None
if hasattr(clause, "_limit_offset_value"):
value = clause._limit_offset_value
return util.asint(value)
else:
return clause
def _make_slice(limit_clause, offset_clause, start, stop):
"""Compute LIMIT/OFFSET in terms of slice start/end
"""
# for calculated limit/offset, try to do the addition of
# values to offset in Python, however if a SQL clause is present
# then the addition has to be on the SQL side.
if start is not None and stop is not None:
offset_clause = _offset_or_limit_clause_asint_if_possible(
offset_clause
)
if offset_clause is None:
offset_clause = 0
if start != 0:
offset_clause = offset_clause + start
if offset_clause == 0:
offset_clause = None
else:
offset_clause = _offset_or_limit_clause(offset_clause)
limit_clause = _offset_or_limit_clause(stop - start)
elif start is None and stop is not None:
limit_clause = _offset_or_limit_clause(stop)
elif start is not None and stop is None:
offset_clause = _offset_or_limit_clause_asint_if_possible(
offset_clause
)
if offset_clause is None:
offset_clause = 0
if start != 0:
offset_clause = offset_clause + start
if offset_clause == 0:
offset_clause = None
else:
offset_clause = _offset_or_limit_clause(offset_clause)
return limit_clause, offset_clause
def _getitem(iterable_query, item):
"""calculate __getitem__ in terms of an iterable query object
that also has a slice() method.
"""
if isinstance(item, slice):
start, stop, step = util.decode_slice(item)
if (
isinstance(stop, int)
and isinstance(start, int)
and stop - start <= 0
):
return []
# perhaps we should execute a count() here so that we
# can still use LIMIT/OFFSET ?
elif (isinstance(start, int) and start < 0) or (
isinstance(stop, int) and stop < 0
):
return list(iterable_query)[item]
res = iterable_query.slice(start, stop)
if step is not None:
return list(res)[None : None : item.step]
else:
return list(res)
else:
if item == -1:
return list(iterable_query)[-1]
else:
return list(iterable_query[item : item + 1])[0]
+74
View File
@@ -571,6 +571,26 @@ class Options(util.with_metaclass(_MetaOptions)):
o1.__dict__.update(other)
return o1
def __eq__(self, other):
# TODO: very inefficient. This is used only in test suites
# right now.
for a, b in util.zip_longest(self._cache_attrs, other._cache_attrs):
if getattr(self, a) != getattr(other, b):
return False
return True
def __repr__(self):
# TODO: fairly inefficient, used only in debugging right now.
return "%s(%s)" % (
self.__class__.__name__,
", ".join(
"%s=%r" % (k, self.__dict__[k])
for k in self._cache_attrs
if k in self.__dict__
),
)
@hybridmethod
def add_to_element(self, name, value):
return self + {name: getattr(self, name) + value}
@@ -610,6 +630,60 @@ class Options(util.with_metaclass(_MetaOptions)):
)
return cls + d
@classmethod
def from_execution_options(
cls, key, attrs, exec_options, statement_exec_options
):
""""process Options argument in terms of execution options.
e.g.::
(
load_options,
execution_options,
) = QueryContext.default_load_options.from_execution_options(
"_sa_orm_load_options",
{
"populate_existing",
"autoflush",
"yield_per"
},
execution_options,
statement._execution_options,
)
get back the Options and refresh "_sa_orm_load_options" in the
exec options dict w/ the Options as well
"""
# common case is that no options we are looking for are
# in either dictionary, so cancel for that first
check_argnames = attrs.intersection(
set(exec_options).union(statement_exec_options)
)
existing_options = exec_options.get(key, cls)
if check_argnames:
result = {}
for argname in check_argnames:
local = "_" + argname
if argname in exec_options:
result[local] = exec_options[argname]
elif argname in statement_exec_options:
result[local] = statement_exec_options[argname]
new_options = existing_options + result
exec_options = util.immutabledict().merge_with(
exec_options, {key: new_options}
)
return new_options, exec_options
else:
return existing_options, exec_options
class CacheableOptions(Options, HasCacheKey):
@hybridmethod
+12
View File
@@ -52,6 +52,18 @@ def _document_text_coercion(paramname, meth_rst, param_rst):
)
def _expression_collection_was_a_list(attrname, fnname, args):
if args and isinstance(args[0], (list, set)) and len(args) == 1:
util.warn_deprecated_20(
'The "%s" argument to %s() is now passed as a series of '
"positional "
"elements, rather than as a list. " % (attrname, fnname)
)
return args[0]
else:
return args
def expect(role, element, apply_propagate_attrs=None, argname=None, **kw):
if (
role.allows_lambda
+40 -22
View File
@@ -2573,10 +2573,8 @@ class Case(ColumnElement):
stmt = select([users_table]).\
where(
case(
[
(users_table.c.name == 'wendy', 'W'),
(users_table.c.name == 'jack', 'J')
],
(users_table.c.name == 'wendy', 'W'),
(users_table.c.name == 'jack', 'J'),
else_='E'
)
)
@@ -2597,7 +2595,10 @@ class Case(ColumnElement):
("else_", InternalTraversal.dp_clauseelement),
]
def __init__(self, whens, value=None, else_=None):
# TODO: for Py2k removal, this will be:
# def __init__(self, *whens, value=None, else_=None):
def __init__(self, *whens, **kw):
r"""Produce a ``CASE`` expression.
The ``CASE`` construct in SQL is a conditional object that
@@ -2612,10 +2613,8 @@ class Case(ColumnElement):
stmt = select([users_table]).\
where(
case(
[
(users_table.c.name == 'wendy', 'W'),
(users_table.c.name == 'jack', 'J')
],
(users_table.c.name == 'wendy', 'W'),
(users_table.c.name == 'jack', 'J'),
else_='E'
)
)
@@ -2660,16 +2659,14 @@ class Case(ColumnElement):
from sqlalchemy import case, literal_column
case(
[
(
orderline.c.qty > 100,
literal_column("'greaterthan100'")
),
(
orderline.c.qty > 10,
literal_column("'greaterthan10'")
)
],
(
orderline.c.qty > 100,
literal_column("'greaterthan100'")
),
(
orderline.c.qty > 10,
literal_column("'greaterthan10'")
),
else_=literal_column("'lessthan10'")
)
@@ -2683,19 +2680,23 @@ class Case(ColumnElement):
ELSE 'lessthan10'
END
:param whens: The criteria to be compared against,
:param \*whens: The criteria to be compared against,
:paramref:`.case.whens` accepts two different forms, based on
whether or not :paramref:`.case.value` is used.
.. versionchanged:: 1.4 the :func:`_sql.case`
function now accepts the series of WHEN conditions positionally;
passing the expressions within a list is deprecated.
In the first form, it accepts a list of 2-tuples; each 2-tuple
consists of ``(<sql expression>, <value>)``, where the SQL
expression is a boolean expression and "value" is a resulting value,
e.g.::
case([
case(
(users_table.c.name == 'wendy', 'W'),
(users_table.c.name == 'jack', 'J')
])
)
In the second form, it accepts a Python dictionary of comparison
values mapped to a resulting value; this form requires
@@ -2720,11 +2721,23 @@ class Case(ColumnElement):
"""
if "whens" in kw:
util.warn_deprecated_20(
'The "whens" argument to case() is now passed as a series of '
"positional "
"elements, rather than as a list. "
)
whens = kw.pop("whens")
else:
whens = coercions._expression_collection_was_a_list(
"whens", "case", whens
)
try:
whens = util.dictlike_iteritems(whens)
except TypeError:
pass
value = kw.pop("value", None)
if value is not None:
whenlist = [
(
@@ -2760,11 +2773,16 @@ class Case(ColumnElement):
self.type = type_
self.whens = whenlist
else_ = kw.pop("else_", None)
if else_ is not None:
self.else_ = coercions.expect(roles.ExpressionElementRole, else_)
else:
self.else_ = None
if kw:
raise TypeError("unknown arguments: %s" % (", ".join(sorted(kw))))
@property
def _from_objects(self):
return list(
-32
View File
@@ -169,9 +169,6 @@ class Operators(object):
:class:`.Boolean`, and those that do not will be of the same
type as the left-hand operand.
.. versionadded:: 1.2.0b3 - added the
:paramref:`.Operators.op.return_type` argument.
.. seealso::
:ref:`types_operators`
@@ -194,8 +191,6 @@ class Operators(object):
:paramref:`.Operators.op.is_comparison`
flag with True.
.. versionadded:: 1.2.0b3
.. seealso::
:meth:`.Operators.op`
@@ -723,15 +718,6 @@ class ColumnOperators(Operators):
With the value of ``:param`` as ``"foo/%bar"``.
.. versionadded:: 1.2
.. versionchanged:: 1.2.0 The
:paramref:`.ColumnOperators.startswith.autoescape` parameter is
now a simple boolean rather than a character; the escape
character itself is also escaped, and defaults to a forwards
slash, which itself can be customized using the
:paramref:`.ColumnOperators.startswith.escape` parameter.
:param escape: a character which when given will render with the
``ESCAPE`` keyword to establish that character as the escape
character. This character can then be placed preceding occurrences
@@ -811,15 +797,6 @@ class ColumnOperators(Operators):
With the value of ``:param`` as ``"foo/%bar"``.
.. versionadded:: 1.2
.. versionchanged:: 1.2.0 The
:paramref:`.ColumnOperators.endswith.autoescape` parameter is
now a simple boolean rather than a character; the escape
character itself is also escaped, and defaults to a forwards
slash, which itself can be customized using the
:paramref:`.ColumnOperators.endswith.escape` parameter.
:param escape: a character which when given will render with the
``ESCAPE`` keyword to establish that character as the escape
character. This character can then be placed preceding occurrences
@@ -899,15 +876,6 @@ class ColumnOperators(Operators):
With the value of ``:param`` as ``"foo/%bar"``.
.. versionadded:: 1.2
.. versionchanged:: 1.2.0 The
:paramref:`.ColumnOperators.contains.autoescape` parameter is
now a simple boolean rather than a character; the escape
character itself is also escaped, and defaults to a forwards
slash, which itself can be customized using the
:paramref:`.ColumnOperators.contains.escape` parameter.
:param escape: a character which when given will render with the
``ESCAPE`` keyword to establish that character as the escape
character. This character can then be placed preceding occurrences
+92 -66
View File
@@ -2225,6 +2225,17 @@ class ForUpdateArg(ClauseElement):
("skip_locked", InternalTraversal.dp_boolean),
]
@classmethod
def _from_argument(cls, with_for_update):
if isinstance(with_for_update, ForUpdateArg):
return with_for_update
elif with_for_update in (None, False):
return None
elif with_for_update is True:
return ForUpdateArg()
else:
return ForUpdateArg(**with_for_update)
def __eq__(self, other):
return (
isinstance(other, ForUpdateArg)
@@ -2699,6 +2710,12 @@ class SelectStatementGrouping(GroupedElement, SelectBase):
class DeprecatedSelectBaseGenerations(object):
"""A collection of methods available on :class:`_sql.Select` and
:class:`_sql.CompoundSelect`, these are all **deprecated** methods as they
modify the object in-place.
"""
@util.deprecated(
"1.4",
"The :meth:`_expression.GenerativeSelect.append_order_by` "
@@ -2740,9 +2757,6 @@ class DeprecatedSelectBaseGenerations(object):
as it
provides standard :term:`method chaining`.
.. seealso::
:meth:`_expression.GenerativeSelect.group_by`
"""
self.group_by.non_generative(self, *clauses)
@@ -3353,6 +3367,12 @@ class CompoundSelect(HasCompileState, GenerativeSelect):
class DeprecatedSelectGenerations(object):
"""A collection of methods available on :class:`_sql.Select`, these
are all **deprecated** methods as they modify the :class:`_sql.Select`
object in -place.
"""
@util.deprecated(
"1.4",
"The :meth:`_expression.Select.append_correlation` "
@@ -3377,7 +3397,7 @@ class DeprecatedSelectGenerations(object):
"1.4",
"The :meth:`_expression.Select.append_column` method is deprecated "
"and will be removed in a future release. Use the generative "
"method :meth:`_expression.Select.column`.",
"method :meth:`_expression.Select.add_columns`.",
)
def append_column(self, column):
"""Append the given column expression to the columns clause of this
@@ -3388,14 +3408,10 @@ class DeprecatedSelectGenerations(object):
my_select.append_column(some_table.c.new_column)
This is an **in-place** mutation method; the
:meth:`_expression.Select.column` method is preferred,
:meth:`_expression.Select.add_columns` method is preferred,
as it provides standard
:term:`method chaining`.
See the documentation for :meth:`_expression.Select.with_only_columns`
for guidelines on adding /replacing the columns of a
:class:`_expression.Select` object.
"""
self.add_columns.non_generative(self, column)
@@ -3501,6 +3517,21 @@ class SelectState(util.MemoizedSlots, CompileState):
self.columns_plus_names = statement._generate_columns_plus_names(True)
@classmethod
def _plugin_not_implemented(cls):
raise NotImplementedError(
"The default SELECT construct without plugins does not "
"implement this method."
)
@classmethod
def get_column_descriptions(cls, statement):
cls._plugin_not_implemented()
@classmethod
def from_statement(cls, statement, from_statement):
cls._plugin_not_implemented()
def _get_froms(self, statement):
seen = set()
froms = []
@@ -3805,6 +3836,15 @@ class Select(
):
"""Represents a ``SELECT`` statement.
The :class:`_sql.Select` object is normally constructed using the
:func:`_sql.select` function. See that function for details.
.. seealso::
:func:`_sql.select`
:ref:`coretutorial_selecting` - in the Core tutorial
"""
__visit_name__ = "select"
@@ -3821,7 +3861,7 @@ class Select(
_from_obj = ()
_auto_correlate = True
compile_options = SelectState.default_select_compile_options
_compile_options = SelectState.default_select_compile_options
_traverse_internals = (
[
@@ -3851,7 +3891,7 @@ class Select(
)
_cache_key_traversal = _traverse_internals + [
("compile_options", InternalTraversal.dp_has_cache_key)
("_compile_options", InternalTraversal.dp_has_cache_key)
]
@classmethod
@@ -4274,12 +4314,35 @@ class Select(
@property
def column_descriptions(self):
"""Return a 'column descriptions' structure which may be
plugin-specific.
:term:`plugin-specific`.
"""
meth = SelectState.get_plugin_class(self).get_column_descriptions
return meth(self)
def from_statement(self, statement):
"""Apply the columns which this :class:`.Select` would select
onto another statement.
This operation is :term:`plugin-specific` and will raise a not
supported exception if this :class:`_sql.Select` does not select from
plugin-enabled entities.
The statement is typically either a :func:`_expression.text` or
:func:`_expression.select` construct, and should return the set of
columns appropriate to the entities represented by this
:class:`.Select`.
.. seealso::
:ref:`orm_tutorial_literal_sql` - usage examples in the
ORM tutorial
"""
meth = SelectState.get_plugin_class(self).from_statement
return meth(self, statement)
@_generative
def join(self, target, onclause=None, isouter=False, full=False):
r"""Create a SQL JOIN against this :class:`_expresson.Select`
@@ -4550,7 +4613,7 @@ class Select(
)
@_generative
def with_only_columns(self, columns):
def with_only_columns(self, *columns):
r"""Return a new :func:`_expression.select` construct with its columns
clause replaced with the given columns.
@@ -4558,65 +4621,26 @@ class Select(
:func:`_expression.select` had been called with the given columns
clause. I.e. a statement::
s = select([table1.c.a, table1.c.b])
s = s.with_only_columns([table1.c.b])
s = select(table1.c.a, table1.c.b)
s = s.with_only_columns(table1.c.b)
should be exactly equivalent to::
s = select([table1.c.b])
s = select(table1.c.b)
This means that FROM clauses which are only derived
from the column list will be discarded if the new column
list no longer contains that FROM::
Note that this will also dynamically alter the FROM clause of the
statement if it is not explicitly stated. To maintain the FROM
clause, ensure the :meth:`_sql.Select.select_from` method is
used appropriately::
>>> table1 = table('t1', column('a'), column('b'))
>>> table2 = table('t2', column('a'), column('b'))
>>> s1 = select([table1.c.a, table2.c.b])
>>> print(s1)
SELECT t1.a, t2.b FROM t1, t2
>>> s2 = s1.with_only_columns([table2.c.b])
>>> print(s2)
SELECT t2.b FROM t1
s = select(table1.c.a, table2.c.b)
s = s.select_from(table2.c.b).with_only_columns(table1.c.a)
The preferred way to maintain a specific FROM clause
in the construct, assuming it won't be represented anywhere
else (i.e. not in the WHERE clause, etc.) is to set it using
:meth:`_expression.Select.select_from`::
:param \*columns: column expressions to be used.
>>> s1 = select([table1.c.a, table2.c.b]).\
... select_from(table1.join(table2,
... table1.c.a==table2.c.a))
>>> s2 = s1.with_only_columns([table2.c.b])
>>> print(s2)
SELECT t2.b FROM t1 JOIN t2 ON t1.a=t2.a
Care should also be taken to use the correct set of column objects
passed to :meth:`_expression.Select.with_only_columns`.
Since the method is
essentially equivalent to calling the :func:`_expression.select`
construct in the first place with the given columns, the columns passed
to :meth:`_expression.Select.with_only_columns`
should usually be a subset of
those which were passed to the :func:`_expression.select`
construct, not those which are available from the ``.c`` collection of
that :func:`_expression.select`. That is::
s = select([table1.c.a, table1.c.b]).select_from(table1)
s = s.with_only_columns([table1.c.b])
and **not**::
# usually incorrect
s = s.with_only_columns([s.c.b])
The latter would produce the SQL::
SELECT b
FROM (SELECT t1.a AS a, t1.b AS b
FROM t1), t1
Since the :func:`_expression.select` construct is essentially
being asked to select both from ``table1`` as well as itself.
.. versionchanged:: 1.4 the :meth:`_sql.Select.with_only_columns`
method accepts the list of column expressions positionally;
passing the expressions as a list is deprecateed.
"""
@@ -4626,7 +4650,9 @@ class Select(
self._assert_no_memoizations()
rc = []
for c in columns:
for c in coercions._expression_collection_was_a_list(
"columns", "Select.with_only_columns", columns
):
c = coercions.expect(roles.ColumnsClauseRole, c,)
# TODO: why are we doing this here?
if isinstance(c, ScalarSelect):
+3
View File
@@ -404,6 +404,9 @@ class AssertsCompiledSQL(object):
from sqlalchemy import orm
if isinstance(clause, orm.dynamic.AppenderQuery):
clause = clause._statement
if isinstance(clause, orm.Query):
compile_state = clause._compile_state()
compile_state.statement._label_style = (
+60 -65
View File
@@ -402,31 +402,30 @@ class MemUsageWBackendTest(EnsureZeroed):
@profile_memory()
def go():
sess = create_session()
a1 = A(col2="a1")
a2 = A(col2="a2")
a3 = A(col2="a3")
a1.bs.append(B(col2="b1"))
a1.bs.append(B(col2="b2"))
a3.bs.append(B(col2="b3"))
for x in [a1, a2, a3]:
sess.add(x)
sess.flush()
sess.expunge_all()
with Session() as sess:
a1 = A(col2="a1")
a2 = A(col2="a2")
a3 = A(col2="a3")
a1.bs.append(B(col2="b1"))
a1.bs.append(B(col2="b2"))
a3.bs.append(B(col2="b3"))
for x in [a1, a2, a3]:
sess.add(x)
sess.commit()
alist = sess.query(A).order_by(A.col1).all()
eq_(
[
A(col2="a1", bs=[B(col2="b1"), B(col2="b2")]),
A(col2="a2", bs=[]),
A(col2="a3", bs=[B(col2="b3")]),
],
alist,
)
alist = sess.query(A).order_by(A.col1).all()
eq_(
[
A(col2="a1", bs=[B(col2="b1"), B(col2="b2")]),
A(col2="a2", bs=[]),
A(col2="a3", bs=[B(col2="b3")]),
],
alist,
)
for a in alist:
sess.delete(a)
sess.flush()
for a in alist:
sess.delete(a)
sess.commit()
go()
@@ -501,33 +500,31 @@ class MemUsageWBackendTest(EnsureZeroed):
"use_reaper": False,
}
)
sess = create_session(bind=engine)
with Session(engine) as sess:
a1 = A(col2="a1")
a2 = A(col2="a2")
a3 = A(col2="a3")
a1.bs.append(B(col2="b1"))
a1.bs.append(B(col2="b2"))
a3.bs.append(B(col2="b3"))
for x in [a1, a2, a3]:
sess.add(x)
sess.commit()
a1 = A(col2="a1")
a2 = A(col2="a2")
a3 = A(col2="a3")
a1.bs.append(B(col2="b1"))
a1.bs.append(B(col2="b2"))
a3.bs.append(B(col2="b3"))
for x in [a1, a2, a3]:
sess.add(x)
sess.flush()
sess.expunge_all()
alist = sess.query(A).order_by(A.col1).all()
eq_(
[
A(col2="a1", bs=[B(col2="b1"), B(col2="b2")]),
A(col2="a2", bs=[]),
A(col2="a3", bs=[B(col2="b3")]),
],
alist,
)
alist = sess.query(A).order_by(A.col1).all()
eq_(
[
A(col2="a1", bs=[B(col2="b1"), B(col2="b2")]),
A(col2="a2", bs=[]),
A(col2="a3", bs=[B(col2="b3")]),
],
alist,
)
for a in alist:
sess.delete(a)
sess.commit()
for a in alist:
sess.delete(a)
sess.flush()
sess.close()
engine.dispose()
go()
@@ -555,29 +552,27 @@ class MemUsageWBackendTest(EnsureZeroed):
mapper(Wide, wide_table, _compiled_cache_size=10)
metadata.create_all()
session = create_session()
w1 = Wide()
session.add(w1)
session.flush()
session.close()
with Session() as session:
w1 = Wide()
session.add(w1)
session.commit()
del session
counter = [1]
@profile_memory()
def go():
session = create_session()
w1 = session.query(Wide).first()
x = counter[0]
dec = 10
while dec > 0:
# trying to count in binary here,
# works enough to trip the test case
if pow(2, dec) < x:
setattr(w1, "col%d" % dec, counter[0])
x -= pow(2, dec)
dec -= 1
session.flush()
session.close()
with Session() as session:
w1 = session.query(Wide).first()
x = counter[0]
dec = 10
while dec > 0:
# trying to count in binary here,
# works enough to trip the test case
if pow(2, dec) < x:
setattr(w1, "col%d" % dec, counter[0])
x -= pow(2, dec)
dec -= 1
session.commit()
counter[0] += 1
try:
+17 -18
View File
@@ -1913,33 +1913,32 @@ class VersioningTest(fixtures.MappedTest):
)
mapper(Sub, subtable, inherits=Base, polymorphic_identity=2)
sess = create_session()
sess = Session(autoflush=False)
b1 = Base(value="b1")
s1 = Sub(value="sub1", subdata="some subdata")
sess.add(b1)
sess.add(s1)
sess.flush()
sess.commit()
sess2 = create_session()
s2 = sess2.query(Base).get(s1.id)
sess2 = Session(autoflush=False)
s2 = sess2.get(Base, s1.id)
s2.subdata = "sess2 subdata"
s1.subdata = "sess1 subdata"
sess.flush()
sess.commit()
assert_raises(
orm_exc.StaleDataError,
sess2.query(Base).with_for_update(read=True).get,
sess2.get,
Base,
s1.id,
with_for_update=dict(read=True),
)
if not testing.db.dialect.supports_sane_rowcount:
sess2.flush()
else:
assert_raises(orm_exc.StaleDataError, sess2.flush)
sess2.rollback()
sess2.refresh(s2)
if testing.db.dialect.supports_sane_rowcount:
@@ -1967,7 +1966,7 @@ class VersioningTest(fixtures.MappedTest):
)
mapper(Sub, subtable, inherits=Base, polymorphic_identity=2)
sess = create_session()
sess = Session(autoflush=False, expire_on_commit=False)
b1 = Base(value="b1")
s1 = Sub(value="sub1", subdata="some subdata")
@@ -1976,21 +1975,21 @@ class VersioningTest(fixtures.MappedTest):
sess.add(s1)
sess.add(s2)
sess.flush()
sess.commit()
sess2 = create_session()
s3 = sess2.query(Base).get(s1.id)
sess2 = Session(autoflush=False, expire_on_commit=False)
s3 = sess2.get(Base, s1.id)
sess2.delete(s3)
sess2.flush()
sess2.commit()
s2.subdata = "some new subdata"
sess.flush()
sess.commit()
s1.subdata = "some new subdata"
if testing.db.dialect.supports_sane_rowcount:
assert_raises(orm_exc.StaleDataError, sess.flush)
assert_raises(orm_exc.StaleDataError, sess.commit)
else:
sess.flush()
sess.commit()
class DistinctPKTest(fixtures.MappedTest):
+125
View File
@@ -3,6 +3,7 @@ from sqlalchemy import and_
from sqlalchemy import cast
from sqlalchemy import desc
from sqlalchemy import event
from sqlalchemy import exc as sa_exc
from sqlalchemy import func
from sqlalchemy import Integer
from sqlalchemy import literal_column
@@ -35,6 +36,8 @@ from sqlalchemy.orm import undefer
from sqlalchemy.orm import with_polymorphic
from sqlalchemy.orm.collections import collection
from sqlalchemy.orm.util import polymorphic_union
from sqlalchemy.sql import elements
from sqlalchemy.testing import assert_raises
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import assertions
from sqlalchemy.testing import AssertsCompiledSQL
@@ -51,6 +54,7 @@ from .inheritance import _poly_fixtures
from .test_events import _RemoveListeners
from .test_options import PathTest as OptionsPathTest
from .test_query import QueryTest
from .test_transaction import _LocalFixture
class DeprecatedQueryTest(_fixtures.FixtureTest, AssertsCompiledSQL):
@@ -510,6 +514,127 @@ class DeprecatedQueryTest(_fixtures.FixtureTest, AssertsCompiledSQL):
)
class SessionTest(fixtures.RemovesEvents, _LocalFixture):
def test_subtransactions_deprecated(self):
s1 = Session(testing.db)
s1.begin()
with testing.expect_deprecated_20(
"The Session.begin.subtransactions flag is deprecated "
"and will be removed in SQLAlchemy version 2.0."
):
s1.begin(subtransactions=True)
s1.close()
def test_autocommit_deprecated(Self):
with testing.expect_deprecated_20(
"The Session.autocommit parameter is deprecated "
"and will be removed in SQLAlchemy version 2.0."
):
Session(autocommit=True)
@testing.requires.independent_connections
@testing.emits_warning(".*previous exception")
def test_failed_rollback_deactivates_transaction_ctx_integration(self):
# test #4050 in the same context as that of oslo.db
User = self.classes.User
with testing.expect_deprecated_20(
"The Session.autocommit parameter is deprecated"
):
session = Session(bind=testing.db, autocommit=True)
evented_exceptions = []
caught_exceptions = []
def canary(context):
evented_exceptions.append(context.original_exception)
rollback_error = testing.db.dialect.dbapi.InterfaceError(
"Can't roll back to savepoint"
)
def prevent_savepoint_rollback(
cursor, statement, parameters, context=None
):
if (
context is not None
and context.compiled
and isinstance(
context.compiled.statement,
elements.RollbackToSavepointClause,
)
):
raise rollback_error
self.event_listen(testing.db, "handle_error", canary, retval=True)
self.event_listen(
testing.db.dialect, "do_execute", prevent_savepoint_rollback
)
with session.begin():
session.add(User(id=1, name="x"))
try:
with session.begin():
try:
with session.begin_nested():
# raises IntegrityError on flush
session.add(User(id=1, name="x"))
# outermost is the failed SAVEPOINT rollback
# from the "with session.begin_nested()"
except sa_exc.DBAPIError as dbe_inner:
caught_exceptions.append(dbe_inner.orig)
raise
except sa_exc.DBAPIError as dbe_outer:
caught_exceptions.append(dbe_outer.orig)
is_true(
isinstance(
evented_exceptions[0], testing.db.dialect.dbapi.IntegrityError
)
)
eq_(evented_exceptions[1], rollback_error)
eq_(len(evented_exceptions), 2)
eq_(caught_exceptions, [rollback_error, rollback_error])
def test_contextmanager_commit(self):
User = self.classes.User
with testing.expect_deprecated_20(
"The Session.autocommit parameter is deprecated"
):
sess = Session(autocommit=True)
with sess.begin():
sess.add(User(name="u1"))
sess.rollback()
eq_(sess.query(User).count(), 1)
def test_contextmanager_rollback(self):
User = self.classes.User
with testing.expect_deprecated_20(
"The Session.autocommit parameter is deprecated"
):
sess = Session(autocommit=True)
def go():
with sess.begin():
sess.add(User()) # name can't be null
assert_raises(sa_exc.DBAPIError, go)
eq_(sess.query(User).count(), 0)
with sess.begin():
sess.add(User(name="u1"))
eq_(sess.query(User).count(), 1)
class DeprecatedInhTest(_poly_fixtures._Polymorphic):
def test_with_polymorphic(self):
Person = _poly_fixtures.Person
+66 -68
View File
@@ -12,10 +12,8 @@ from sqlalchemy.orm import configure_mappers
from sqlalchemy.orm import create_session
from sqlalchemy.orm import exc as orm_exc
from sqlalchemy.orm import mapper
from sqlalchemy.orm import Query
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
from sqlalchemy.orm.dynamic import AppenderMixin
from sqlalchemy.testing import assert_raises
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import AssertsCompiledSQL
@@ -67,6 +65,62 @@ class _DynamicFixture(object):
mapper(Item, items)
return Order, Item
def _user_order_item_fixture(self):
(
users,
Keyword,
items,
order_items,
item_keywords,
Item,
User,
keywords,
Order,
orders,
) = (
self.tables.users,
self.classes.Keyword,
self.tables.items,
self.tables.order_items,
self.tables.item_keywords,
self.classes.Item,
self.classes.User,
self.tables.keywords,
self.classes.Order,
self.tables.orders,
)
mapper(
User,
users,
properties={
"orders": relationship(
Order, order_by=orders.c.id, lazy="dynamic"
)
},
)
mapper(
Order,
orders,
properties={
"items": relationship(
Item, secondary=order_items, order_by=items.c.id
),
},
)
mapper(
Item,
items,
properties={
"keywords": relationship(
Keyword, secondary=item_keywords
) # m2m
},
)
mapper(Keyword, keywords)
return User, Order, Item, Keyword
class DynamicTest(_DynamicFixture, _fixtures.FixtureTest, AssertsCompiledSQL):
def test_basic(self):
@@ -117,11 +171,10 @@ class DynamicTest(_DynamicFixture, _fixtures.FixtureTest, AssertsCompiledSQL):
sess = create_session()
u = sess.query(User).get(8)
sess.expunge(u)
assert_raises(
orm_exc.DetachedInstanceError,
u.addresses.filter_by,
email_address="e",
)
q = u.addresses.filter_by(email_address="e")
assert_raises(orm_exc.DetachedInstanceError, q.first)
def test_no_uselist_false(self):
User, Address = self._user_address_fixture(
@@ -450,6 +503,12 @@ class DynamicTest(_DynamicFixture, _fixtures.FixtureTest, AssertsCompiledSQL):
use_default_dialect=True,
)
@testing.combinations(
# lambda
)
def test_join_syntaxes(self, expr):
User, Order, Item, Keyword = self._user_order_item_fixture()
def test_transient_count(self):
User, Address = self._user_address_fixture()
u1 = User()
@@ -462,67 +521,6 @@ class DynamicTest(_DynamicFixture, _fixtures.FixtureTest, AssertsCompiledSQL):
u1.addresses.append(Address())
eq_(u1.addresses[0], Address())
def test_custom_query(self):
class MyQuery(Query):
pass
User, Address = self._user_address_fixture(
addresses_args={"query_class": MyQuery}
)
sess = create_session()
u = User()
sess.add(u)
col = u.addresses
assert isinstance(col, Query)
assert isinstance(col, MyQuery)
assert hasattr(col, "append")
eq_(type(col).__name__, "AppenderMyQuery")
q = col.limit(1)
assert isinstance(q, Query)
assert isinstance(q, MyQuery)
assert not hasattr(q, "append")
eq_(type(q).__name__, "MyQuery")
def test_custom_query_with_custom_mixin(self):
class MyAppenderMixin(AppenderMixin):
def add(self, items):
if isinstance(items, list):
for item in items:
self.append(item)
else:
self.append(items)
class MyQuery(Query):
pass
class MyAppenderQuery(MyAppenderMixin, MyQuery):
query_class = MyQuery
User, Address = self._user_address_fixture(
addresses_args={"query_class": MyAppenderQuery}
)
sess = create_session()
u = User()
sess.add(u)
col = u.addresses
assert isinstance(col, Query)
assert isinstance(col, MyQuery)
assert hasattr(col, "append")
assert hasattr(col, "add")
eq_(type(col).__name__, "MyAppenderQuery")
q = col.limit(1)
assert isinstance(q, Query)
assert isinstance(q, MyQuery)
assert not hasattr(q, "append")
assert not hasattr(q, "add")
eq_(type(q).__name__, "MyQuery")
class UOWTest(
_DynamicFixture, _fixtures.FixtureTest, testing.AssertsExecutionResults
+38 -3
View File
@@ -1212,6 +1212,38 @@ class InstancesTest(QueryTest, AssertsCompiledSQL):
self.assert_sql_count(testing.db, go, 1)
def test_contains_eager_four_future(self):
users, addresses, User = (
self.tables.users,
self.tables.addresses,
self.classes.User,
)
sess = create_session(future=True)
selectquery = users.outerjoin(addresses).select(
users.c.id < 10,
use_labels=True,
order_by=[users.c.id, addresses.c.id],
)
q = select(User)
def go():
result = (
sess.execute(
q.options(contains_eager("addresses")).from_statement(
selectquery
)
)
.scalars()
.unique()
.all()
)
assert self.static.user_address_result[0:3] == result
self.assert_sql_count(testing.db, go, 1)
def test_contains_eager_aliased(self):
User, Address = self.classes.User, self.classes.Address
@@ -2122,14 +2154,17 @@ class MixedEntitiesTest(QueryTest, AssertsCompiledSQL):
(user10, None),
]
sess = create_session()
sess = create_session(future=True)
selectquery = users.outerjoin(addresses).select(
use_labels=True, order_by=[users.c.id, addresses.c.id]
)
result = sess.execute(
select(User, Address).from_statement(selectquery)
)
eq_(
list(sess.query(User, Address).from_statement(selectquery)),
expected,
list(result), expected,
)
sess.expunge_all()
+6 -6
View File
@@ -85,7 +85,7 @@ class PathTest(object):
ent.entity_zero
for ent in q._compile_state()._mapper_entities
],
q.compile_options._current_path,
q._compile_options._current_path,
attr,
False,
)
@@ -1432,7 +1432,7 @@ class PickleTest(PathTest, QueryTest):
ent.entity_zero
for ent in query._compile_state()._mapper_entities
],
query.compile_options._current_path,
query._compile_options._current_path,
attr,
False,
)
@@ -1469,7 +1469,7 @@ class PickleTest(PathTest, QueryTest):
ent.entity_zero
for ent in query._compile_state()._mapper_entities
],
query.compile_options._current_path,
query._compile_options._current_path,
attr,
False,
)
@@ -1514,7 +1514,7 @@ class LocalOptsTest(PathTest, QueryTest):
for tb in opt._to_bind:
tb._bind_loader(
[ent.entity_zero for ent in ctx._mapper_entities],
query.compile_options._current_path,
query._compile_options._current_path,
attr,
False,
)
@@ -1608,7 +1608,7 @@ class SubOptionsTest(PathTest, QueryTest):
ent.entity_zero
for ent in q._compile_state()._mapper_entities
],
q.compile_options._current_path,
q._compile_options._current_path,
attr_a,
False,
)
@@ -1622,7 +1622,7 @@ class SubOptionsTest(PathTest, QueryTest):
ent.entity_zero
for ent in q._compile_state()._mapper_entities
],
q.compile_options._current_path,
q._compile_options._current_path,
attr_b,
False,
)
+248
View File
@@ -12,6 +12,7 @@ from sqlalchemy import collate
from sqlalchemy import column
from sqlalchemy import desc
from sqlalchemy import distinct
from sqlalchemy import event
from sqlalchemy import exc as sa_exc
from sqlalchemy import exists
from sqlalchemy import ForeignKey
@@ -52,6 +53,7 @@ from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
from sqlalchemy.orm import subqueryload
from sqlalchemy.orm import synonym
from sqlalchemy.orm.context import QueryContext
from sqlalchemy.orm.util import join
from sqlalchemy.orm.util import with_parent
from sqlalchemy.sql import expression
@@ -560,6 +562,22 @@ class BindSensitiveStringifyTest(fixtures.TestBase):
class GetTest(QueryTest):
def test_loader_options(self):
User = self.classes.User
s = Session()
u1 = s.query(User).options(joinedload(User.addresses)).get(8)
eq_(len(u1.__dict__["addresses"]), 3)
def test_loader_options_future(self):
User = self.classes.User
s = Session()
u1 = s.get(User, 8, options=[joinedload(User.addresses)])
eq_(len(u1.__dict__["addresses"]), 3)
def test_get_composite_pk_keyword_based_no_result(self):
CompositePk = self.classes.CompositePk
@@ -610,6 +628,18 @@ class GetTest(QueryTest):
u2 = s.query(User).get(7)
assert u is not u2
def test_get_future(self):
User = self.classes.User
s = create_session()
assert s.get(User, 19) is None
u = s.get(User, 7)
u2 = s.get(User, 7)
assert u is u2
s.expunge_all()
u2 = s.get(User, 7)
assert u is not u2
def test_get_composite_pk_no_result(self):
CompositePk = self.classes.CompositePk
@@ -843,6 +873,73 @@ class GetTest(QueryTest):
assert u.addresses[0].email_address == "jack@bean.com"
assert u.orders[1].items[2].description == "item 5"
def test_populate_existing_future(self):
User, Address = self.classes.User, self.classes.Address
s = Session(future=True, autoflush=False)
userlist = s.query(User).all()
u = userlist[0]
u.name = "foo"
a = Address(name="ed")
u.addresses.append(a)
self.assert_(a in u.addresses)
stmt = select(User).execution_options(populate_existing=True)
s.execute(stmt,).scalars().all()
self.assert_(u not in s.dirty)
self.assert_(u.name == "jack")
self.assert_(a not in u.addresses)
u.addresses[0].email_address = "lala"
u.orders[1].items[2].description = "item 12"
# test that lazy load doesn't change child items
s.query(User).populate_existing().all()
assert u.addresses[0].email_address == "lala"
assert u.orders[1].items[2].description == "item 12"
# eager load does
stmt = (
select(User)
.options(
joinedload("addresses"),
joinedload("orders").joinedload("items"),
)
.execution_options(populate_existing=True)
)
s.execute(stmt).scalars().all()
assert u.addresses[0].email_address == "jack@bean.com"
assert u.orders[1].items[2].description == "item 5"
def test_option_transfer_future(self):
User = self.classes.User
stmt = select(User).execution_options(
populate_existing=True, autoflush=False, yield_per=10
)
s = Session(testing.db, future=True)
m1 = mock.Mock()
event.listen(s, "do_orm_execute", m1)
s.execute(stmt)
eq_(
m1.mock_calls[0].args[0].load_options,
QueryContext.default_load_options(
_autoflush=False, _populate_existing=True, _yield_per=10
),
)
class InvalidGenerationsTest(QueryTest, AssertsCompiledSQL):
@testing.combinations(
@@ -4339,6 +4436,31 @@ class TextTest(QueryTest, AssertsCompiledSQL):
None,
)
def test_select_star_future(self):
User = self.classes.User
sess = Session(future=True)
eq_(
sess.execute(
select(User).from_statement(
text("select * from users order by id")
)
)
.scalars()
.first(),
User(id=7),
)
eq_(
sess.execute(
select(User).from_statement(
text("select * from users where name='nonexistent'")
)
)
.scalars()
.first(),
None,
)
def test_columns_mismatched(self):
# test that columns using column._label match, as well as that
# ordering doesn't matter
@@ -4360,6 +4482,27 @@ class TextTest(QueryTest, AssertsCompiledSQL):
],
)
def test_columns_mismatched_future(self):
# test that columns using column._label match, as well as that
# ordering doesn't matter
User = self.classes.User
s = create_session(future=True)
q = select(User).from_statement(
text(
"select name, 27 as foo, id as users_id from users order by id"
)
)
eq_(
s.execute(q).scalars().all(),
[
User(id=7, name="jack"),
User(id=8, name="ed"),
User(id=9, name="fred"),
User(id=10, name="chuck"),
],
)
def test_columns_multi_table_uselabels(self):
# test that columns using column._label match, as well as that
# ordering doesn't matter.
@@ -4385,6 +4528,31 @@ class TextTest(QueryTest, AssertsCompiledSQL):
],
)
def test_columns_multi_table_uselabels_future(self):
# test that columns using column._label match, as well as that
# ordering doesn't matter.
User = self.classes.User
Address = self.classes.Address
s = create_session(future=True)
q = select(User, Address).from_statement(
text(
"select users.name AS users_name, users.id AS users_id, "
"addresses.id AS addresses_id FROM users JOIN addresses "
"ON users.id = addresses.user_id WHERE users.id=8 "
"ORDER BY addresses.id"
)
)
eq_(
s.execute(q).all(),
[
(User(id=8), Address(id=2)),
(User(id=8), Address(id=3)),
(User(id=8), Address(id=4)),
],
)
def test_columns_multi_table_uselabels_contains_eager(self):
# test that columns using column._label match, as well as that
# ordering doesn't matter.
@@ -4411,6 +4579,32 @@ class TextTest(QueryTest, AssertsCompiledSQL):
self.assert_sql_count(testing.db, go, 1)
def test_columns_multi_table_uselabels_contains_eager_future(self):
# test that columns using column._label match, as well as that
# ordering doesn't matter.
User = self.classes.User
Address = self.classes.Address
s = create_session(future=True)
q = (
select(User)
.from_statement(
text(
"select users.name AS users_name, users.id AS users_id, "
"addresses.id AS addresses_id FROM users JOIN addresses "
"ON users.id = addresses.user_id WHERE users.id=8 "
"ORDER BY addresses.id"
)
)
.options(contains_eager(User.addresses))
)
def go():
r = s.execute(q).unique().scalars().all()
eq_(r[0].addresses, [Address(id=2), Address(id=3), Address(id=4)])
self.assert_sql_count(testing.db, go, 1)
def test_columns_multi_table_uselabels_cols_contains_eager(self):
# test that columns using column._label match, as well as that
# ordering doesn't matter.
@@ -4437,6 +4631,32 @@ class TextTest(QueryTest, AssertsCompiledSQL):
self.assert_sql_count(testing.db, go, 1)
def test_columns_multi_table_uselabels_cols_contains_eager_future(self):
# test that columns using column._label match, as well as that
# ordering doesn't matter.
User = self.classes.User
Address = self.classes.Address
s = create_session(future=True)
q = (
select(User)
.from_statement(
text(
"select users.name AS users_name, users.id AS users_id, "
"addresses.id AS addresses_id FROM users JOIN addresses "
"ON users.id = addresses.user_id WHERE users.id=8 "
"ORDER BY addresses.id"
).columns(User.name, User.id, Address.id)
)
.options(contains_eager(User.addresses))
)
def go():
r = s.execute(q).unique().scalars().all()
eq_(r[0].addresses, [Address(id=2), Address(id=3), Address(id=4)])
self.assert_sql_count(testing.db, go, 1)
def test_textual_select_orm_columns(self):
# test that columns using column._label match, as well as that
# ordering doesn't matter.
@@ -4521,6 +4741,34 @@ class TextTest(QueryTest, AssertsCompiledSQL):
[User(id=9)],
)
def test_whereclause_future(self):
User = self.classes.User
s = create_session(future=True)
eq_(
s.execute(select(User).filter(text("id in (8, 9)")))
.scalars()
.all(),
[User(id=8), User(id=9)],
)
eq_(
s.execute(
select(User).filter(text("name='fred'")).filter(text("id=9"))
)
.scalars()
.all(),
[User(id=9)],
)
eq_(
s.execute(
select(User).filter(text("name='fred'")).filter(User.id == 9)
)
.scalars()
.all(),
[User(id=9)],
)
def test_binds_coerce(self):
User = self.classes.User
+4 -10
View File
@@ -1345,19 +1345,13 @@ class CompositeSelfRefFKTest(fixtures.MappedTest, AssertsCompiledSQL):
e1 = sess.query(Employee).filter_by(name="emp1").one()
e5 = sess.query(Employee).filter_by(name="emp5").one()
test_e1 = sess.query(Employee).get([c1.company_id, e1.emp_id])
test_e1 = sess.get(Employee, [c1.company_id, e1.emp_id])
assert test_e1.name == "emp1", test_e1.name
test_e5 = sess.query(Employee).get([c2.company_id, e5.emp_id])
test_e5 = sess.get(Employee, [c2.company_id, e5.emp_id])
assert test_e5.name == "emp5", test_e5.name
assert [x.name for x in test_e1.employees] == ["emp2", "emp3"]
assert (
sess.query(Employee).get([c1.company_id, 3]).reports_to.name
== "emp1"
)
assert (
sess.query(Employee).get([c2.company_id, 3]).reports_to.name
== "emp5"
)
assert sess.get(Employee, [c1.company_id, 3]).reports_to.name == "emp1"
assert sess.get(Employee, [c2.company_id, 3]).reports_to.name == "emp5"
def _test_join_aliasing(self, sess):
Employee = self.classes.Employee
+33 -3
View File
@@ -174,8 +174,7 @@ class TransScopingTest(_fixtures.FixtureTest):
assert_raises_message(
sa.exc.InvalidRequestError,
"A transaction is already begun. Use "
"subtransactions=True to allow subtransactions.",
"A transaction is already begun on this Session.",
s.begin,
)
@@ -625,7 +624,7 @@ class SessionStateTest(_fixtures.FixtureTest):
session.flush()
session.commit()
def test_active_flag(self):
def test_active_flag_autocommit(self):
sess = create_session(bind=config.db, autocommit=True)
assert not sess.is_active
sess.begin()
@@ -633,6 +632,37 @@ class SessionStateTest(_fixtures.FixtureTest):
sess.rollback()
assert not sess.is_active
def test_active_flag_autobegin(self):
sess = create_session(bind=config.db, autocommit=False)
assert sess.is_active
assert not sess.in_transaction()
sess.begin()
assert sess.is_active
sess.rollback()
assert sess.is_active
def test_active_flag_autobegin_future(self):
sess = create_session(bind=config.db, future=True)
assert sess.is_active
assert not sess.in_transaction()
sess.begin()
assert sess.is_active
sess.rollback()
assert sess.is_active
def test_active_flag_partial_rollback(self):
sess = create_session(bind=config.db, autocommit=False)
assert sess.is_active
assert not sess.in_transaction()
sess.begin()
assert sess.is_active
sess.begin(_subtrans=True)
sess.rollback()
assert not sess.is_active
sess.rollback()
assert sess.is_active
@engines.close_open_connections
def test_add_delete(self):
User, Address, addresses, users = (
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -754,12 +754,12 @@ class PassiveDeletesTest(fixtures.MappedTest):
)
mapper(MyClass, mytable)
session = create_session()
session = Session()
mc = MyClass()
mco = MyOtherClass()
mco.myclass = mc
session.add(mco)
session.flush()
session.commit()
eq_(session.scalar(select(func.count("*")).select_from(mytable)), 1)
eq_(
@@ -769,7 +769,7 @@ class PassiveDeletesTest(fixtures.MappedTest):
session.expire(mco, ["myclass"])
session.delete(mco)
session.flush()
session.commit()
# mytable wasn't deleted, is the point.
eq_(session.scalar(select(func.count("*")).select_from(mytable)), 1)
+55 -69
View File
@@ -57,13 +57,8 @@ class CaseTest(fixtures.TestBase, AssertsCompiledSQL):
inner = select(
[
case(
[
[info_table.c.pk < 3, "lessthan3"],
[
and_(info_table.c.pk >= 3, info_table.c.pk < 7),
"gt3",
],
]
(info_table.c.pk < 3, "lessthan3"),
(and_(info_table.c.pk >= 3, info_table.c.pk < 7), "gt3"),
).label("x"),
info_table.c.pk,
info_table.c.info,
@@ -80,14 +75,17 @@ class CaseTest(fixtures.TestBase, AssertsCompiledSQL):
# gt3 4 pk_4_data
# gt3 5 pk_5_data
# gt3 6 pk_6_data
assert inner_result == [
("lessthan3", 1, "pk_1_data"),
("lessthan3", 2, "pk_2_data"),
("gt3", 3, "pk_3_data"),
("gt3", 4, "pk_4_data"),
("gt3", 5, "pk_5_data"),
("gt3", 6, "pk_6_data"),
]
eq_(
inner_result,
[
("lessthan3", 1, "pk_1_data"),
("lessthan3", 2, "pk_2_data"),
("gt3", 3, "pk_3_data"),
("gt3", 4, "pk_4_data"),
("gt3", 5, "pk_5_data"),
("gt3", 6, "pk_6_data"),
],
)
outer = select([inner.alias("q_inner")])
@@ -105,10 +103,8 @@ class CaseTest(fixtures.TestBase, AssertsCompiledSQL):
w_else = select(
[
case(
[
[info_table.c.pk < 3, cast(3, Integer)],
[and_(info_table.c.pk >= 3, info_table.c.pk < 6), 6],
],
[info_table.c.pk < 3, cast(3, Integer)],
[and_(info_table.c.pk >= 3, info_table.c.pk < 6), 6],
else_=0,
).label("x"),
info_table.c.pk,
@@ -119,21 +115,24 @@ class CaseTest(fixtures.TestBase, AssertsCompiledSQL):
else_result = w_else.execute().fetchall()
assert else_result == [
(3, 1, "pk_1_data"),
(3, 2, "pk_2_data"),
(6, 3, "pk_3_data"),
(6, 4, "pk_4_data"),
(6, 5, "pk_5_data"),
(0, 6, "pk_6_data"),
]
eq_(
else_result,
[
(3, 1, "pk_1_data"),
(3, 2, "pk_2_data"),
(6, 3, "pk_3_data"),
(6, 4, "pk_4_data"),
(6, 5, "pk_5_data"),
(0, 6, "pk_6_data"),
],
)
def test_literal_interpretation_ambiguous(self):
assert_raises_message(
exc.ArgumentError,
r"Column expression expected, got 'x'",
case,
[("x", "y")],
("x", "y"),
)
def test_literal_interpretation_ambiguous_tuple(self):
@@ -141,18 +140,18 @@ class CaseTest(fixtures.TestBase, AssertsCompiledSQL):
exc.ArgumentError,
r"Column expression expected, got \('x', 'y'\)",
case,
[(("x", "y"), "z")],
(("x", "y"), "z"),
)
def test_literal_interpretation(self):
t = table("test", column("col1"))
self.assert_compile(
case([("x", "y")], value=t.c.col1),
case(("x", "y"), value=t.c.col1),
"CASE test.col1 WHEN :param_1 THEN :param_2 END",
)
self.assert_compile(
case([(t.c.col1 == 7, "y")], else_="z"),
case((t.c.col1 == 7, "y"), else_="z"),
"CASE WHEN (test.col1 = :col1_1) THEN :param_1 ELSE :param_2 END",
)
@@ -162,7 +161,7 @@ class CaseTest(fixtures.TestBase, AssertsCompiledSQL):
select(
[
case(
[(info_table.c.info == "pk_4_data", text("'yes'"))],
(info_table.c.info == "pk_4_data", text("'yes'")),
else_=text("'no'"),
)
]
@@ -170,36 +169,20 @@ class CaseTest(fixtures.TestBase, AssertsCompiledSQL):
select(
[
case(
[
(
info_table.c.info == "pk_4_data",
literal_column("'yes'"),
)
],
(
info_table.c.info == "pk_4_data",
literal_column("'yes'"),
),
else_=literal_column("'no'"),
)
]
).order_by(info_table.c.info),
]:
if testing.against("firebird"):
eq_(
s.execute().fetchall(),
[
("no ",),
("no ",),
("no ",),
("yes",),
("no ",),
("no ",),
],
)
else:
eq_(
s.execute().fetchall(),
[("no",), ("no",), ("no",), ("yes",), ("no",), ("no",)],
)
eq_(
s.execute().fetchall(),
[("no",), ("no",), ("no",), ("yes",), ("no",), ("no",)],
)
@testing.fails_on("firebird", "FIXME: unknown")
def testcase_with_dict(self):
query = select(
[
@@ -215,24 +198,27 @@ class CaseTest(fixtures.TestBase, AssertsCompiledSQL):
],
from_obj=[info_table],
)
assert query.execute().fetchall() == [
("lessthan3", 1, "pk_1_data"),
("lessthan3", 2, "pk_2_data"),
("gt3", 3, "pk_3_data"),
("gt3", 4, "pk_4_data"),
("gt3", 5, "pk_5_data"),
("gt3", 6, "pk_6_data"),
]
simple_query = select(
eq_(
query.execute().fetchall(),
[
("lessthan3", 1, "pk_1_data"),
("lessthan3", 2, "pk_2_data"),
("gt3", 3, "pk_3_data"),
("gt3", 4, "pk_4_data"),
("gt3", 5, "pk_5_data"),
("gt3", 6, "pk_6_data"),
],
)
simple_query = (
select(
case(
{1: "one", 2: "two"}, value=info_table.c.pk, else_="other"
),
info_table.c.pk,
],
whereclause=info_table.c.pk < 4,
from_obj=[info_table],
)
.where(info_table.c.pk < 4)
.select_from(info_table)
)
assert simple_query.execute().fetchall() == [
+7 -12
View File
@@ -320,20 +320,15 @@ class CoreFixtures(object):
ClauseList(table_a.c.a == 5, table_a.c.b == table_a.c.a),
),
lambda: (
case(whens=[(table_a.c.a == 5, 10), (table_a.c.a == 10, 20)]),
case(whens=[(table_a.c.a == 18, 10), (table_a.c.a == 10, 20)]),
case(whens=[(table_a.c.a == 5, 10), (table_a.c.b == 10, 20)]),
case((table_a.c.a == 5, 10), (table_a.c.a == 10, 20)),
case((table_a.c.a == 18, 10), (table_a.c.a == 10, 20)),
case((table_a.c.a == 5, 10), (table_a.c.b == 10, 20)),
case(
whens=[
(table_a.c.a == 5, 10),
(table_a.c.b == 10, 20),
(table_a.c.a == 9, 12),
]
),
case(
whens=[(table_a.c.a == 5, 10), (table_a.c.a == 10, 20)],
else_=30,
(table_a.c.a == 5, 10),
(table_a.c.b == 10, 20),
(table_a.c.a == 9, 12),
),
case((table_a.c.a == 5, 10), (table_a.c.a == 10, 20), else_=30,),
case({"wendy": "W", "jack": "J"}, value=table_a.c.a, else_="E"),
case({"wendy": "W", "jack": "J"}, value=table_a.c.b, else_="E"),
case({"wendy_w": "W", "jack": "J"}, value=table_a.c.a, else_="E"),
+1 -1
View File
@@ -4050,7 +4050,7 @@ class KwargPropagationTest(fixtures.TestBase):
self._do_test(s)
def test_case(self):
c = case([(self.criterion, self.column)], else_=self.column)
c = case((self.criterion, self.column), else_=self.column)
self._do_test(c)
def test_cast(self):
+107 -1
View File
@@ -3,6 +3,7 @@
from sqlalchemy import alias
from sqlalchemy import and_
from sqlalchemy import bindparam
from sqlalchemy import case
from sqlalchemy import CHAR
from sqlalchemy import column
from sqlalchemy import create_engine
@@ -488,6 +489,111 @@ class SelectableTest(fixtures.TestBase, AssertsCompiledSQL):
"SELECT anon_1.a FROM (SELECT 1 AS a ORDER BY 1) AS anon_1",
)
def test_case_list_legacy(self):
t1 = table("t", column("q"))
with testing.expect_deprecated(
r"The \"whens\" argument to case\(\) is now passed"
):
stmt = select(t1).where(
case(
[(t1.c.q == 5, "foo"), (t1.c.q == 10, "bar")], else_="bat"
)
!= "bat"
)
self.assert_compile(
stmt,
"SELECT t.q FROM t WHERE CASE WHEN (t.q = :q_1) "
"THEN :param_1 WHEN (t.q = :q_2) THEN :param_2 "
"ELSE :param_3 END != :param_4",
)
def test_case_whens_kw(self):
t1 = table("t", column("q"))
with testing.expect_deprecated(
r"The \"whens\" argument to case\(\) is now passed"
):
stmt = select(t1).where(
case(
whens=[(t1.c.q == 5, "foo"), (t1.c.q == 10, "bar")],
else_="bat",
)
!= "bat"
)
self.assert_compile(
stmt,
"SELECT t.q FROM t WHERE CASE WHEN (t.q = :q_1) "
"THEN :param_1 WHEN (t.q = :q_2) THEN :param_2 "
"ELSE :param_3 END != :param_4",
)
def test_case_whens_dict_kw(self):
t1 = table("t", column("q"))
with testing.expect_deprecated(
r"The \"whens\" argument to case\(\) is now passed"
):
stmt = select(t1).where(
case(whens={t1.c.q == 5: "foo"}, else_="bat",) != "bat"
)
self.assert_compile(
stmt,
"SELECT t.q FROM t WHERE CASE WHEN (t.q = :q_1) THEN "
":param_1 ELSE :param_2 END != :param_3",
)
def test_case_kw_arg_detection(self):
# because we support py2k, case() has to parse **kw for now
assert_raises_message(
TypeError,
"unknown arguments: bat, foo",
case,
(column("x") == 10, 5),
else_=15,
foo="bar",
bat="hoho",
)
def test_with_only_generative(self):
table1 = table(
"table1",
column("col1"),
column("col2"),
column("col3"),
column("colx"),
)
s1 = table1.select().scalar_subquery()
with testing.expect_deprecated(
r"The \"columns\" argument to "
r"Select.with_only_columns\(\) is now passed"
):
stmt = s1.with_only_columns([s1])
self.assert_compile(
stmt,
"SELECT (SELECT table1.col1, table1.col2, "
"table1.col3, table1.colx FROM table1) AS anon_1",
)
def test_from_list_with_columns(self):
table1 = table("t1", column("a"))
table2 = table("t2", column("b"))
s1 = select(table1.c.a, table2.c.b)
self.assert_compile(s1, "SELECT t1.a, t2.b FROM t1, t2")
with testing.expect_deprecated(
r"The \"columns\" argument to "
r"Select.with_only_columns\(\) is now passed"
):
s2 = s1.with_only_columns([table2.c.b])
self.assert_compile(s2, "SELECT t2.b FROM t2")
def test_column(self):
stmt = select(column("x"))
with testing.expect_deprecated(
@@ -815,7 +921,7 @@ class DeprecatedAppendMethTest(fixtures.TestBase, AssertsCompiledSQL):
def test_append_column(self):
t1 = table("t1", column("q"), column("p"))
stmt = select(t1.c.q)
with self._expect_deprecated("Select", "column", "column"):
with self._expect_deprecated("Select", "column", "add_columns"):
stmt.append_column(t1.c.p)
self.assert_compile(stmt, "SELECT t1.q, t1.p FROM t1")
+3 -3
View File
@@ -479,7 +479,7 @@ class SelectableTest(
def test_with_only_generative(self):
s1 = table1.select().scalar_subquery()
self.assert_compile(
s1.with_only_columns([s1]),
s1.with_only_columns(s1),
"SELECT (SELECT table1.col1, table1.col2, "
"table1.col3, table1.colx FROM table1) AS anon_1",
)
@@ -1165,12 +1165,12 @@ class SelectableTest(
table2 = table("t2", column("b"))
s1 = select(table1.c.a, table2.c.b)
self.assert_compile(s1, "SELECT t1.a, t2.b FROM t1, t2")
s2 = s1.with_only_columns([table2.c.b])
s2 = s1.with_only_columns(table2.c.b)
self.assert_compile(s2, "SELECT t2.b FROM t2")
s3 = sql_util.ClauseAdapter(table1).traverse(s1)
self.assert_compile(s3, "SELECT t1.a, t2.b FROM t1, t2")
s4 = s3.with_only_columns([table2.c.b])
s4 = s3.with_only_columns(table2.c.b)
self.assert_compile(s4, "SELECT t2.b FROM t2")
def test_from_list_against_existing_one(self):