]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Documentation updates for 1.4
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 28 Jun 2020 15:59:34 +0000 (11:59 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 6 Aug 2020 02:19:46 +0000 (22:19 -0400)
* major additions to 1.4 migration doc; removed additional
  verbosity regarding caching methodology and reorganized the
  doc to present itself more as a "what's changed" guide

* as we now have a path for asyncio, update that doc so that
  we aren't spreading obsolete information

* updates to the 2.0 migration guide with latest info, however
  this is still an architecture doc and not a migration guide
  yet, will need further rework.

* start really talking about 1.x vs. 2.0 style everywhere.  Querying
  is most of the docs so this is going to be a prominent
  theme, start getting it to fit in

* Add introductory documentation for ORM example sections as these
  are too sparse

* new documentation for do_orm_execute(), many separate sections,
  adding deprecation notes to before_compile() and similar

* new example suites to illustrate do_orm_execute(),
  with_loader_criteria()

* modernized horizontal sharding examples and added a separate
  example to distinguish between multiple databases and single
  database w/ multiple tables use case

* introducing DEEP ALCHEMY, will use zzzeeksphinx 1.1.6

* no name for the alchemist yet however the dragon's name
  is FlambĂ©

Change-Id: Id6b5c03b1ce9ddb7b280f66792212a0ef0a1c541

35 files changed:
doc/build/changelog/migration_14.rst
doc/build/changelog/migration_20.rst
doc/build/changelog/unreleased_14/4472.rst
doc/build/conf.py
doc/build/core/connections.rst
doc/build/glossary.rst
doc/build/index.rst
doc/build/orm/events.rst
doc/build/orm/examples.rst
doc/build/orm/extending.rst
doc/build/orm/extensions/baked.rst
doc/build/orm/internals.rst
doc/build/orm/mapped_attributes.rst
doc/build/orm/query.rst
doc/build/orm/session_api.rst
doc/build/orm/session_basics.rst
doc/build/orm/session_events.rst
doc/build/orm/session_transaction.rst
doc/build/orm/tutorial.rst
examples/dogpile_caching/__init__.py
examples/extending_query/__init__.py [new file with mode: 0644]
examples/extending_query/filter_public.py [new file with mode: 0644]
examples/extending_query/temporal_range.py [new file with mode: 0644]
examples/sharding/__init__.py
examples/sharding/separate_databases.py [moved from examples/sharding/attribute_shard.py with 79% similarity]
examples/sharding/separate_tables.py [new file with mode: 0644]
lib/sqlalchemy/engine/interfaces.py
lib/sqlalchemy/engine/result.py
lib/sqlalchemy/event/api.py
lib/sqlalchemy/event/legacy.py
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/events.py
lib/sqlalchemy/orm/loading.py
lib/sqlalchemy/orm/session.py
test/base/test_utils.py

index 08ff190b8388b208537c3889633cc941614eb16a..4207e4e813a09002fefedcde99cfed0cedeca4b6 100644 (file)
@@ -23,6 +23,118 @@ What's New in SQLAlchemy 1.4?
 Behavioral Changes - General
 ============================
 
+.. _change_5159:
+
+ORM Query is internally unified with select, update, delete; 2.0 style execution available
+------------------------------------------------------------------------------------------
+
+The biggest conceptual change to SQLAlchemy for version 2.0 and essentially
+in 1.4 as well is that the great separation between the :class:`_sql.Select`
+construct in Core and the :class:`_orm.Query` object in the ORM has been removed,
+as well as between the :meth:`_orm.Query.update` and :meth:`_orm.Query.delete`
+methods in how they relate to :class:`_dml.Update` and :class:`_dml.Delete`.
+
+With regards to :class:`_sql.Select` and :class:`_orm.Query`, these two objects
+have for many versions had similar, largely overlapping APIs and even some
+ability to change between one and the other, while remaining very different in
+their usage patterns and behaviors.   The historical background for this was
+that the :class:`_orm.Query` object was introduced to overcome shortcomings in
+the :class:`_sql.Select` object which used to be at the core of how ORM objects
+were queried, except that they had to be queried in terms of
+:class:`_schema.Table` metadata only.    However :class:`_orm.Query` had only a
+simplistic interface for loading objects, and only over the course of many
+major releases did it eventually gain most of the flexibility of the
+:class:`_sql.Select` object, which then led to the ongoing awkwardness that
+these two objects became highly similar yet still largely incompatible with
+each other.
+
+In version 1.4, all Core and ORM SELECT statements are rendered from a
+:class:`_sql.Select` object directly; when the :class:`_orm.Query` object
+is used, at statement invocation time it copies its state to a :class:`_sql.Select`
+which is then invoked internally using :term:`2.0 style` execution.   Going forward,
+the :class:`_orm.Query` object will become legacy only, and applications will
+be encouraged to move to :term:`2.0 style` execution which allows Core constructs
+to be used freely against ORM entities::
+
+    with Session(engine, future=True) as sess:
+
+        stmt = select(User).where(
+            User.name == 'sandy'
+        ).join(User.addresses).where(Address.email_address.like("%gmail%"))
+
+        result = sess.execute(stmt)
+
+        for user in result.scalars():
+            print(user)
+
+Things to note about the above example:
+
+* The :class:`_orm.Session` and :class:`_orm.sessionmaker` objects now feature
+  full context manager (i.e. the ``with:`` statement) capability;
+  see the revised documentation at :ref:`session_getting` for an example.
+
+* Within the 1.4 series, all :term:`2.0 style` ORM invocation uses a
+  :class:`_orm.Session` that includes the :paramref:`_orm.Session.future`
+  flag set to ``True``; this flag indicates the :class:`_orm.Session` should
+  have 2.0-style behaviors, which include that ORM queries can be invoked
+  from :class:`_orm.Session.execute` as well as some changes in transactional
+  features.   In version 2.0 this flag will always be ``True``.
+
+* The :func:`_sql.select` construct no longer needs brackets around the
+  columns clause; see :ref:`change_5284` for background on this improvement.
+
+* The :func:`_sql.select`  / :class:`_sql.Select` object has a :meth:`_sql.Select.join`
+  method that acts like that of the :class:`_orm.Query` and even accommodates
+  an ORM relationship attribute (without breaking the separation between
+  Core and ORM!) - see :ref:`change_select_join` for background on this.
+
+* Statements that work with ORM entities and are expected to return ORM
+  results are invoked using :meth:`.orm.Session.execute`.  See
+  :ref:`session_querying_20` for a primer.
+
+* a :class:`_engine.Result` object is returned, rather than a plain list, which
+  itself is a much more sophisticated version of the previous ``ResultProxy``
+  object; this object is now used both for Core and ORM results.   See
+  :ref:`change_result_14_core`,
+  :ref:`change_4710_core`, and :ref:`change_4710_orm` for information on this.
+
+Throughout SQLAlchemy's documentation, there will be many references to
+:term:`1.x style` and :term:`2.0 style` execution.  This is to distinguish
+between the two querying styles and to attempt to forwards-document the new
+calling style going forward.  In SQLAlchemy 2.0, while the :class:`_orm.Query`
+object may remain as a legacy construct, it will no longer be featured in
+most documentation.
+
+Similar adjustments have been made to "bulk updates and deletes" such that
+Core :func:`_sql.update` and :func:`_sql.delete` can be used for bulk
+operations.   A bulk update like the following::
+
+    session.query(User).filter(User.name == 'sandy').update({"password": "foobar"}, synchronize_session="fetch")
+
+can now be achieved in :term:`2.0 style` (and indeed the above runs internally
+in this way) as follows::
+
+    with Session(engine, future=True) as sess:
+        stmt = update(User).where(
+            User.name == 'sandy'
+        ).values(password="foobar").execution_options(
+            synchronize_session="fetch"
+        )
+
+        sess.execute(stmt)
+
+Note the use of the :meth:`_sql.Executable.execution_options` method to pass
+ORM-related options.  The use of "execution options" is now much more prevalent
+within both Core and ORM, and many ORM-related methods from :class:`_orm.Query`
+are now implemented as execution options (see :meth:`_orm.Query.execution_options`
+for some examples).
+
+.. seealso::
+
+    :ref:`migration_20_toplevel`
+
+:ticket:`5159`
+
 .. _change_4639:
 
 Transparent SQL Compilation Caching added to All DQL, DML Statements in Core, ORM
@@ -34,7 +146,7 @@ systems from the base of Core all the way through ORM now allows the
 majority of Python computation involved producing SQL strings and related
 statement metadata from a user-constructed statement to be cached in memory,
 such that subsequent invocations of an identical statement construct will use
-35-60% fewer resources.
+35-60% fewer CPU resources.
 
 This caching goes beyond the construction of the SQL string to also include the
 construction of result fetching structures that link the SQL construct to the
@@ -66,23 +178,17 @@ In 1.4, the code above without modification completes::
 This first test indicates that regular ORM queries when using caching can run
 over many iterations in the range of **30% faster**.
 
-"Baked Query" style construction now available for all Core and ORM Queries
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-The "Baked Query" extension has been in SQLAlchemy for several years and
-provides a caching system that is based on defining segments of SQL statements
-within Python functions, so that the functions both serve as cache keys
-(since they uniquely and persistently identify a specific line in the
-source code) as well as that they allow the construction of a statement
-to be deferred so that it only need to be invoked once, rather than every
-time the query is rendered.   The functionality of "Baked Query" is now a native
-part of the new caching system, which is available by simply using Python
-functions, typically lambda expressions, either inside of a statement,
-or on the outside using the ``lambda_stmt()`` function that works just
-like a Baked Query.
+A second variant of the feature is the optional use of Python lambdas to defer
+the construction of the query itself.  This is a more sophisticated variant of
+the approach used by the "Baked Query" extension, which was introduced in
+version 1.0.0.     The "lambda" feature may be used in a style very similar to
+that of baked queries, except that it is available in an ad-hoc way for any SQL
+construct.  It additionally includes the ability to scan each invocation of the
+lambda for bound literal values that change on every invocation, as well as
+changes to other constructs, such as querying from a different entity or column
+each time, while still not having to run the actual code each time.
 
-Making use of the newer 2.0 style of using ``select()`` and adding the use
-of **optional** lambdas to defer the computation::
+Using this API looks as follows::
 
     session = Session(bind=engine)
     for id_ in random.sample(ids, n):
@@ -96,127 +202,17 @@ The code above completes::
 
 This test indicates that using the newer "select()" style of ORM querying,
 in conjunction with a full "baked" style invocation that caches the entire
-construction, can run over many iterations in the range of **60% faster**.
-This performance is roughly the same as what the Baked Query extension
-provides as well.  The new approach effectively supersedes the Baked Query
-extension.
+construction, can run over many iterations in the range of **60% faster** and
+grants performace about the same as the baked query system which is now superseded
+by the native caching system.
 
-For comparison, a Baked Query looks like the following::
-
-    bakery = baked.bakery()
-    s = Session(bind=engine)
-    for id_ in random.sample(ids, n):
-        q = bakery(lambda s: s.query(Customer))
-        q += lambda q: q.filter(Customer.id == bindparam("id"))
-        q(s).params(id=id_).one()
-
-The new API allows the same very fast "baked query" approach of building up a
-statement with lambdas, but does not require any other syntactical changes from
-regular statements.  It also no longer requires that "bindparam()" is used for
-literal values that may change; the "closure" of the Python function is scanned
-on every call to extract Python literal values that should be turned into
-parameters.
+The new system makes use of the existing
+:paramref:`_engine.Connection.execution_options.compiled_cache` execution
+option and also adds a cache to the :class:`_engine.Engine` directly, which is
+configured using the :paramref:`_engine.Engine.query_cache_size` parameter.
 
-Methodology Overview
-^^^^^^^^^^^^^^^^^^^^
-
-SQLAlchemy has also for many years included a "compiled_cache" option that is
-used internally by the ORM flush process as well as the Baked Query extension,
-which caches a SQL expression object based on the identity of the object
-itself.  That is, if you create a particular select() object and make use of
-the compiled cache feature, if you pass the same select() object each time, the
-SQL compilation would be cached.  This feature was of limited use since
-SQLAlchemy's programming paradigm is based on the continuous construction of
-new SQL expression objects each time one is required.
-
-The new caching feature uses the same "compiled_cache", however instead of
-using the statement object itself as the cache key, a separate tuple-oriented
-cache key is generated which represents the complete structure of the
-statement.   Two SQL constructs that are composed in exactly the same way will
-produce the same cache key, independent of the bound parameter values that are
-bundled with the statement; these are collected separately from each statement
-and are used when the cached SQL is executed.   The ORM ``Query`` integrates by
-producing a ``select()`` object from itself that is interpreted as an
-ORM-enabled SELECT within the SQL compilation process that occurs beyond the
-cache boundary.
-
-A general listing of architectural changes needed to support this feature:
-
-* The system by which arguments passed to SQL constructs are type-checked and
-  coerced into their desired form was rewritten from an ad-hoc and disorganized
-  system into the ``sqlalchemy.sql.roles`` and
-  ``sqlalchemy.sql.coercions`` modules which provide a type-based approach
-  to the task of composing SQL expression objects, error handling, coercion
-  of objects such as turning SELECT statements into subqueries, as well as
-  integrating with a new "plugin" system that allows SQL constructs to include
-  ORM functionality.
-
-* The system by which clause expressions constructs are iterated and compared
-  from an object structure point of view was also
-  rewritten from one which was ad-hoc and inconsistent into a complete system
-  within the new ``sqlalchemy.sql.traversals`` module.   A test suite was added
-  which ensures that all SQL construction objects include fully consistent
-  comparison and iteration behavior.   This work began with :ticket:`4336`.
-
-* The new iteration system naturally gave rise to the cache-key creation
-  system, which also uses a performance-optimized version of the
-  ``sqlalchemy.sql.traversals`` module to generate a deterministic cache key
-  for any SQL expression based on its structure.   Two instances of a SQL
-  expression that represent the same SQL structure, such as ``select(table('x',
-  column('q'))).where(column('z') > 5)``, are guaranteed to produce the same
-  cache key, independent of the bound parameters which for this statement would
-  be one parameter with the value "5".   Two instances of a SQL expression
-  where any elements are different will produce different cache keys.   When
-  the cache key is generated, the parameters are also collected which will be
-  used to formulate the final parameter list.  This work was completed over
-  many merges and was overall related to :ticket:`4639`.
-
-* The mechanism by which statements such as ``select()`` generate expensive
-  collections and datamembers that are only used for SQL compilation, such
-  as the list of columns and their labels, were organized into a new
-  decoupled system called ``CompileState``.
-
-* All elements of queries that needed to be made compatible with the concept of
-  deterministic SQL compilation were updated, including an expansion of the
-  "postcompile" concept used to render individual parameters inside of "IN"
-  expressions first included in 1.3 as well as alterations to how dialects like
-  the SQL Server dialect render LIMIT / OFFSET expressions that are not
-  compatible with bound parameters.
-
-* The ORM ``Query`` object was fully refactored such that all of the intense
-  computation which would previously occur whenever methods of ``Query`` were
-  called, such as the construction of the ``Query`` itself, when methods
-  ``filter()`` or ``join()`` would be called, etc., was completely reorganized
-  to take place within the ``CompileState`` architecture, meaning the ORM
-  process that generates a Core ``select()`` to render now takes place
-  **within** the SQL compilation process, beyond the caching boundary.  More
-  detail on this change is at
-  :ref:`change_deferred_construction`.
-
-* The ``Query`` object was unified with the ``select()`` object, such that
-  these two objects now have cross-compatible internal state.   The ``Query``
-  can turn itself into a ``select()`` that generates ORM queries by copying its
-  ``__dict__`` into a new ``Select`` object.
-
-* The 2.0-style :class:`.Result` object as well as the "future" version of
-  :class:`_engine.Engine` were developed and integrated into Core and later
-  the ORM also integrated on top of :class:`.Result`.
-
-* The Core and ORM execution models were completely reworked to integrate the
-  new cache key system, and in particular the ORM ``Query`` was reworked such
-  that its execution model now produces a ``Select`` which is passed to
-  ``Session.execute()``, which then invokes the 2.0-style execution model that
-  allows the ``Select`` to be processed as an ORM query beyond the caching
-  boundary.
-
-* Other systems such as ``Query`` bulk updates and deletes, the horizontal
-  sharding extension, the Baked Query extension, and the dogpile caching
-  example were updated to integrate with the new execution model and a new
-  event hook :meth:`.SessionEvents.do_orm_execute` has been added.
-
-* The caching has been enabled via the :paramref:`.create_engine.query_cache_size`
-  parameter, new logging features were added, and the "lambda" argument
-  construction module was added.
+A significant portion of API and behavioral changes throughout 1.4 were
+driven in order to support this new feature.
 
 .. seealso::
 
@@ -232,8 +228,8 @@ A general listing of architectural changes needed to support this feature:
 .. _change_deferred_construction:
 
 
-Many Core and ORM statement objects now perform much of their validation in the compile phase
----------------------------------------------------------------------------------------------
+Many Core and ORM statement objects now perform much of their construction and validation in the compile phase
+--------------------------------------------------------------------------------------------------------------
 
 A major initiative in the 1.4 series is to approach the model of both Core SQL
 statements as well as the ORM Query to allow for an efficient, cacheable model
@@ -241,12 +237,17 @@ of statement creation and compilation, where the compilation step would be
 cached, based on a cache key generated by the created statement object, which
 itself is newly created for each use.  Towards this goal, much of the Python
 computation which occurs within the construction of statements, particularly
-the ORM :class:`_query.Query`, is being moved to occur later, when the
-statement is actually compiled, and additionally that it will only occur if the
-compiled form of the statement is not already cached.   This means that some of
-the error messages which can arise based on arguments passed to the object will
-no longer be raised immediately, and instead will occur only when the statement
-is invoked and its compiled form is not yet cached.
+that of the ORM :class:`_query.Query` as well as the :func:`_sql.select`
+construct when used to invoke ORM queries, is being moved to occur within
+the compilation phase of the statement which only occurs after the statement
+has been invoked, and only if the statement's compiled form was not yet
+cached.
+
+From an end-user perspective, this means that some of the error messages which
+can arise based on arguments passed to the object will no longer be raised
+immediately, and instead will occur only when the statement is invoked for
+the first time.    These conditions are always structural and not data driven,
+so there is no risk of such a condition being missed due to a cached statement.
 
 Error conditions which fall under this category include:
 
@@ -286,8 +287,8 @@ instead.
 
 :ticket:`4689`
 
-API Changes - Core
-==================
+API and Behavioral Changes - Core
+==================================
 
 .. _change_4617:
 
@@ -318,93 +319,70 @@ Raising::
     got <...Select object ...>. To create a FROM clause from a <class
     'sqlalchemy.sql.selectable.Select'> object, use the .subquery() method.
 
-The correct calling form is instead::
+The correct calling form is instead (noting also that :ref:`brackets are no
+longer required for select() <change_5284>`)::
 
-    sq1 = select([user.c.id, user.c.name]).subquery()
-    stmt2 = select([addresses, sq1]).select_from(addresses.join(sq1))
+    sq1 = select(user.c.id, user.c.name).subquery()
+    stmt2 = select(addresses, sq1).select_from(addresses.join(sq1))
 
 Noting above that the :meth:`_expression.SelectBase.subquery` method is essentially
 equivalent to using the :meth:`_expression.SelectBase.alias` method.
 
-The above calling form is typically required in any case as the call to
-:meth:`_expression.SelectBase.subquery` or :meth:`_expression.SelectBase.alias` is needed to
-ensure the subquery has a name.  The MySQL and PostgreSQL databases do not
-accept unnamed subqueries in the FROM clause and they are of limited use
-on other platforms; this is described further below.
-
-Along with the above change, the general capability of :func:`_expression.select` and
-related constructs to create unnamed subqueries, which means a FROM subquery
-that renders without any name i.e. "AS somename", has been removed, and the
-ability of the :func:`_expression.select` construct to implicitly create subqueries
-without explicit calling code to do so is mostly deprecated.   In the above
-example, as has always been the case, using the :meth:`_expression.SelectBase.alias`
-method as well as the new :meth:`_expression.SelectBase.subquery` method without passing a
-name will generate a so-called "anonymous" name, which is the familiar
-``anon_1`` name we see in SQLAlchemy queries::
 
-    SELECT
-        addresses.id, addresses.email, addresses.user_id,
-        anon_1.id, anon_1.name
-    FROM
-    addresses JOIN
-    (SELECT users.id AS id, users.name AS name FROM users) AS anon_1
-    ON addresses.user_id = anon_1.id
-
-Unnamed subqueries in the FROM clause (which note are different from
-so-called "scalar subqueries" which take the place of a column expression
-in the columns clause or WHERE clause) are of extremely limited use in SQL,
-and their production in SQLAlchemy has mostly presented itself as an
-undesirable behavior that needs to be worked around.    For example,
-both the MySQL and PostgreSQL outright reject the usage of unnamed subqueries::
-
-    # MySQL / MariaDB:
-
-    MariaDB [(none)]> select * from (select 1);
-    ERROR 1248 (42000): Every derived table must have its own alias
-
-
-    # PostgreSQL:
-
-    test=> select * from (select 1);
-    ERROR:  subquery in FROM must have an alias
-    LINE 1: select * from (select 1);
-                          ^
-    HINT:  For example, FROM (SELECT ...) [AS] foo.
-
-A database like SQLite accepts them, however it is still often the case that
-the names produced from such a subquery are too ambiguous to be useful::
-
-    sqlite> CREATE TABLE a(id integer);
-    sqlite> CREATE TABLE b(id integer);
-    sqlite> SELECT * FROM a JOIN (SELECT * FROM b) ON a.id=id;
-    Error: ambiguous column name: id
-    sqlite> SELECT * FROM a JOIN (SELECT * FROM b) ON a.id=b.id;
-    Error: no such column: b.id
-
-    # use a name
-    sqlite> SELECT * FROM a JOIN (SELECT * FROM b) AS anon_1 ON a.id=anon_1.id;
-
-Due to the above limitations, there are very few places in SQLAlchemy where
-such a query form was valid; the one exception was within the Oracle dialect
-where they were used to create OFFSET / LIMIT subqueries as Oracle does not
-support these keywords directly; this implementation has been replaced by
-one which uses anonymous subqueries.   Throughout the ORM, exception cases
-that detect where a SELECT statement would be SELECTed from either encourage
-the user to, or implicitly create, an anonymously named subquery; it is hoped
-by moving to an all-explicit subquery much of the complexity incurred by
-these areas can be removed.
-
-As :class:`_expression.SelectBase` objects are no longer :class:`_expression.FromClause` objects,
-attributes like the ``.c`` attribute as well as methods like ``.select()``,
-``.join()``, and ``.outerjoin()`` upon :class:`_expression.SelectBase` are now
-deprecated, as these methods all imply implicit production of a subquery.
-Instead, as is already what the vast majority of applications have to do
-in any case, invoking :meth:`_expression.SelectBase.alias` or :meth:`_expression.SelectBase.subquery`
-will provide for a :class:`.Subquery` object that provides all these attributes,
-as it is part of the :class:`_expression.FromClause` hierarchy.   In the interim, these
-methods are still available, however they now produce an anonymously named
-subquery rather than an unnamed one, and this subquery is distinct from the
-:class:`_expression.SelectBase` construct itself.
+The rationale for this change is based on the following:
+
+* In order to support the unification of :class:`_sql.Select` with
+  :class:`_orm.Query`, the :class:`_sql.Select` object needs to have
+  :meth:`_sql.Select.join` and :meth:`_sql.Select.outerjoin` methods that
+  actually add JOIN criteria to the existing FROM clause, as is what users have
+  always expected it to do in any case.    The previous behavior, having to
+  align with what a :class:`.FromClause` would do, was that it would generate
+  an unnamed subquery and then JOIN to it, which was a completely useless
+  feature that only confused those users unfortunate enough to try this.  This
+  change is discussed at :ref:`change_select_join`.
+
+* The behavior of including a SELECT in the FROM clause of another SELECT
+  without first creating an alias or subquery would be that it creates an
+  unnamed subquery.   While standard SQL does support this syntax, in practice
+  it is rejected by most databases.  For example, both the MySQL and PostgreSQL
+  outright reject the usage of unnamed subqueries::
+
+      # MySQL / MariaDB:
+
+      MariaDB [(none)]> select * from (select 1);
+      ERROR 1248 (42000): Every derived table must have its own alias
+
+
+      # PostgreSQL:
+
+      test=> select * from (select 1);
+      ERROR:  subquery in FROM must have an alias
+      LINE 1: select * from (select 1);
+                            ^
+      HINT:  For example, FROM (SELECT ...) [AS] foo.
+
+  A database like SQLite accepts them, however it is still often the case that
+  the names produced from such a subquery are too ambiguous to be useful::
+
+      sqlite> CREATE TABLE a(id integer);
+      sqlite> CREATE TABLE b(id integer);
+      sqlite> SELECT * FROM a JOIN (SELECT * FROM b) ON a.id=id;
+      Error: ambiguous column name: id
+      sqlite> SELECT * FROM a JOIN (SELECT * FROM b) ON a.id=b.id;
+      Error: no such column: b.id
+
+      # use a name
+      sqlite> SELECT * FROM a JOIN (SELECT * FROM b) AS anon_1 ON a.id=anon_1.id;
+
+  ..
+
+As :class:`_expression.SelectBase` objects are no longer
+:class:`_expression.FromClause` objects, attributes like the ``.c`` attribute
+as well as methods like ``.select()`` is now deprecated, as they imply implicit
+production of a subquery. The ``.join()`` and ``.outerjoin()`` methods are now
+:ref:`repurposed to append JOIN criteria to the existing query <change_select_join>` in a similar
+way as that of :meth:`_orm.Query.join`, which is what users have always
+expected these methods to do in any case.
 
 In place of the ``.c`` attribute, a new attribute :attr:`_expression.SelectBase.selected_columns`
 is added.  This attribute resolves to a column collection that is what most
@@ -427,36 +405,100 @@ present in the ``users.c`` collection::
     stmt = select([users])
     stmt = stmt.where(stmt.selected_columns.name == 'foo')
 
-There is of course the notion that perhaps ``.c`` on :class:`_expression.SelectBase` could
-simply act the way :attr:`_expression.SelectBase.selected_columns` does above, however in
-light of the fact that ``.c`` is strongly associated with the :class:`_expression.FromClause`
-hierarchy, meaning that it is a set of columns that can be directly in the
-FROM clause of another SELECT, it's better that a column collection that
-serves an entirely different purpose have a new name.
-
-In the bigger picture, the reason this change is being made now is towards the
-goal of unifying the ORM :class:`_query.Query` object into the :class:`_expression.SelectBase`
-hierarchy in SQLAlchemy 2.0, so that the ORM will have a "``select()``"
-construct that extends directly from the existing :func:`_expression.select` object,
-having the same methods and behaviors except that it will have additional ORM
-functionality.   All statement objects in Core will also be fully cacheable
-using a new system that resembles "baked queries" except that it will work
-transparently for all statements across Core and ORM.   In order to achieve
-this, the Core class hierarchy needs to be refined to behave in such a way that
-is more easily compatible with the ORM, and the ORM class hierarchy needs to be
-refined so that it is more compatible with Core.
-
 
 :ticket:`4617`
 
 
+.. _change_select_join:
+
+select().join() and outerjoin() add JOIN criteria to the current query, rather than creating a subquery
+-------------------------------------------------------------------------------------------------------
+
+Towards the goal of unifying :class:`_orm.Query` and :class:`_sql.Select`,
+particularly for :term:`2.0 style` use of :class:`_sql.Select`, it was critical
+that there be a working :meth:`_sql.Select.join` method that behaves like the
+:meth:`_orm.Query.join` method, adding additional entries to the FROM clause of
+the existing SELECT and then returning the new :class:`_sql.Select` object for
+further modification, instead of wrapping the object inside of an unnamed
+subquery and returning a JOIN from that subquery, a behavior that has always
+been virtually useless and completely misleading to users.
+
+To allow this to be the case, :ref:`change_4617` was first implemented which
+splits off :class:`_sql.Select` from having to be a :class:`_sql.FromClause`;
+this removed the requirement that :meth:`_sql.Select.join` would need to
+return a :class:`_sql.Join` object rather than a new version of that
+:class:`_sql.Select` object that includes a new JOIN in its FROM clause.
+
+From that point on, as the :meth:`_sql.Select.join` and :meth:`_sql.Select.outerjoin`
+did have an existing behavior, the original plan was that these
+methods would be deprecated, and the new "useful" version of
+the methods would be available on an alternate, "future" :class:`_sql.Select`
+object available as a separate import.
+
+However, after some time working with this particular codebase, it was decided
+that having two different kinds of :class:`_sql.Select` objects floating
+around, each with 95% the same behavior except for some subtle difference
+in how some of the methods behave was going to be more misleading and inconvenient
+than simply making a hard change in how these two methods behave, given
+that the existing behavior of :meth:`_sql.Select.join` and :meth:`_sql.Select.outerjoin`
+is essentially never used and only causes confusion.
+
+So it was decided, given how very useless the current behavior is, and how
+extremely useful and important and useful the new behavior would be, to make a
+**hard behavioral change** in this one area, rather than waiting another year
+and having a more awkward API in the interim.   SQLAlchemy developers do not
+take it lightly to make a completely breaking change like this, however this is
+a very special case and it is extremely unlikely that the previous
+implementation of these methods was being used;  as noted in
+:ref:`change_4617`, major databases such as MySQL and PostgreSQL don't allow
+for unnamed subqueries in any case and from a syntactical point of view it's
+nearly impossible for a JOIN from an unnamed subquery to be useful since it's
+very difficult to refer to the columns within it unambiguously.
+
+With the new implementation, :meth:`_sql.Select.join` and
+:meth:`_sql.Select.outerjoin` now behave very similarly to that of
+:meth:`_orm.Query.join`, adding JOIN criteria to the existing statement by
+matching to the left entity::
+
+    stmt = select(user_table).join(addresses_table, user_table.c.id == addresses_table.c.user_id)
+
+producing::
+
+    SELECT user.id, user.name FROM user JOIN address ON user.id=address.user_id
+
+As is the case for :class:`_sql.Join`, the ON clause is automatically determined
+if feasible::
+
+    stmt = select(user_table).join(addresses_table)
+
+When ORM entities are used in the statement, this is essentially how ORM
+queries are built up using :term:`2.0 style` invocation.  ORM entities will
+assign a "plugin" to the statement internally such that ORM-related compilation
+rules will take place when the statement is compiled into a SQL string. More
+directly, the :meth:`_sql.Select.join` method can accommodate ORM
+relationships, without breaking the hard separation between Core and ORM
+internals::
+
+    stmt = select(User).join(User.addresses)
+
+Another new method :meth:`_sql.Select.join_from` is also added, which
+allows easier specification of the left and right side of a join at once::
+
+    stmt = select(Address.email_address, User.name).join_from(User, Address)
+
+producing::
+
+    SELECT address.email_address, user.name FROM user JOIN address ON user.id == address.user_id
+
+
 .. _change_5284:
 
-select() now accepts positional expressions
--------------------------------------------
+select(), case() now accept positional expressions
+---------------------------------------------------
 
-The :func:`.select` construct will now accept "columns clause"
-arguments positionally::
+As it may be seen elsewhere in this document, the :func:`_sql.select` construct will
+now accept "columns clause" arguments positionally, rather than requiring they
+be passed as a list::
 
     # new way, supports 2.0
     stmt = select(table.c.col1, table.c.col2, ...)
@@ -472,7 +514,8 @@ to function, which passes the list of columns or other expressions as a list::
     stmt = select([table.c.col1, table.c.col2, ...])
 
 The above legacy calling style also accepts the old keyword arguments that have
-since been removed from most narrative documentation::
+since been removed from most narrative documentation.  The existence of these
+keyword arguments is why the columns clause was passed as a list in the first place::
 
     # very much the old way, but still works in 1.4
     stmt = select([table.c.col1, table.c.col2, ...], whereclause=table.c.col1 == 5)
@@ -489,12 +532,28 @@ As part of this change, the :class:`.Select` construct also gains the 2.0-style
 "future" API which includes an updated :meth:`.Select.join` method as well
 as methods like :meth:`.Select.filter_by` and :meth:`.Select.join_from`.
 
+In a related change, the :func:`_sql.case` construct has also been modified
+to accept its list of WHEN clauses positionally, with a similar deprecation
+track for the old calling style::
+
+    stmt = select(users_table).where(
+        case(
+            (users_table.c.name == 'wendy', 'W'),
+            (users_table.c.name == 'jack', 'J'),
+            else_='E'
+        )
+    )
+
+The convention for SQLAlchemy constructs accepting ``*args`` vs. a list of
+values, as is the latter case for a construct like
+:meth:`_sql.ColumnOperators.in_`, is that **positional arguments are used for
+structural specification, lists are used for data specification**.
+
+
 .. seealso::
 
     :ref:`error_c9ae`
 
-    :ref:`migration_20_toplevel`
-
 
 :ticket:`5284`
 
@@ -606,6 +665,134 @@ details.
 
 :ticket:`4645`
 
+.. _change_4737:
+
+
+Built-in FROM linting will warn for any potential cartesian products in a SELECT statement
+------------------------------------------------------------------------------------------
+
+As the Core expression language as well as the ORM are built on an "implicit
+FROMs" model where a particular FROM clause is automatically added if any part
+of the query refers to it, a common issue is the case where a SELECT statement,
+either a top level statement or an embedded subquery, contains FROM elements
+that are not joined to the rest of the FROM elements in the query, causing
+what's referred to as a "cartesian product" in the result set, i.e. every
+possible combination of rows from each FROM element not otherwise joined.  In
+relational databases, this is nearly always an undesirable outcome as it
+produces an enormous result set full of duplicated, uncorrelated data.
+
+SQLAlchemy, for all of its great features, is particularly prone to this sort
+of issue happening as a SELECT statement will have elements added to its FROM
+clause automatically from any table seen in the other clauses. A typical
+scenario looks like the following, where two tables are JOINed together,
+however an additional entry in the WHERE clause that perhaps inadvertently does
+not line up with these two tables will create an additional FROM entry::
+
+    address_alias = aliased(Address)
+
+    q = session.query(User).\
+        join(address_alias, User.addresses).\
+        filter(Address.email_address == 'foo')
+
+The above query selects from a JOIN of ``User`` and ``address_alias``, the
+latter of which is an alias of the ``Address`` entity.  However, the
+``Address`` entity is used within the WHERE clause directly, so the above would
+result in the SQL::
+
+    SELECT
+        users.id AS users_id, users.name AS users_name,
+        users.fullname AS users_fullname,
+        users.nickname AS users_nickname
+    FROM addresses, users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
+    WHERE addresses.email_address = :email_address_1
+
+In the above SQL, we can see what SQLAlchemy developers term "the dreaded
+comma", as we see "FROM addresses, users JOIN addresses" in the FROM clause
+which is the classic sign of a cartesian product; where a query is making use
+of JOIN in order to join FROM clauses together, however because one of them is
+not joined, it uses a comma.      The above query will return a full set of
+rows that join the "user" and "addresses" table together on the "id / user_id"
+column, and will then apply all those rows into a cartesian product against
+every row in the "addresses" table directly.   That is, if there are ten user
+rows and 100 rows in addresses, the above query will return its expected result
+rows, likely to be 100 as all address rows would be selected, multiplied by 100
+again, so that the total result size would be 10000 rows.
+
+The "table1, table2 JOIN table3" pattern is one that also occurs quite
+frequently within the SQLAlchemy ORM due to either subtle mis-application of
+ORM features particularly those related to joined eager loading or joined table
+inheritance, as well as a result of SQLAlchemy ORM bugs within those same
+systems.   Similar issues apply to SELECT statements that use "implicit joins",
+where the JOIN keyword is not used and instead each FROM element is linked with
+another one via the WHERE clause.
+
+For some years there has been a recipe on the Wiki that applies a graph
+algorithm to a :func:`_expression.select` construct at query execution time and inspects
+the structure of the query for these un-linked FROM clauses, parsing through
+the WHERE clause and all JOIN clauses to determine how FROM elements are linked
+together and ensuring that all the FROM elements are connected in a single
+graph. This recipe has now been adapted to be part of the :class:`.SQLCompiler`
+itself where it now optionally emits a warning for a statement if this
+condition is detected.   The warning is enabled using the
+:paramref:`_sa.create_engine.enable_from_linting` flag and is enabled by default.
+The computational overhead of the linter is very low, and additionally it only
+occurs during statement compilation which means for a cached SQL statement it
+only occurs once.
+
+Using this feature, our ORM query above will emit a warning::
+
+    >>> q.all()
+    SAWarning: SELECT statement has a cartesian product between FROM
+    element(s) "addresses_1", "users" and FROM element "addresses".
+    Apply join condition(s) between each element to resolve.
+
+The linter feature accommodates not just for tables linked together through the
+JOIN clauses but also through the WHERE clause  Above, we can add a WHERE
+clause to link the new ``Address`` entity with the previous ``address_alias``
+entity and that will remove the warning::
+
+    q = session.query(User).\
+        join(address_alias, User.addresses).\
+        filter(Address.email_address == 'foo').\
+        filter(Address.id == address_alias.id)  # resolve cartesian products,
+                                                # will no longer warn
+
+The cartesian product warning considers **any** kind of link between two
+FROM clauses to be a resolution, even if the end result set is still
+wasteful, as the linter is intended only to detect the common case of a
+FROM clause that is completely unexpected.  If the FROM clause is referred
+to explicitly elsewhere and linked to the other FROMs, no warning is emitted::
+
+    q = session.query(User).\
+        join(address_alias, User.addresses).\
+        filter(Address.email_address == 'foo').\
+        filter(Address.id > address_alias.id)  # will generate a lot of rows,
+                                               # but no warning
+
+Full cartesian products are also allowed if they are explicitly stated; if we
+wanted for example the cartesian product of ``User`` and ``Address``, we can
+JOIN on :func:`.true` so that every row will match with every other; the
+following query will return all rows and produce no warnings::
+
+    from sqlalchemy import true
+
+    # intentional cartesian product
+    q = session.query(User).join(Address, true())  # intentional cartesian product
+
+The warning is only generated by default when the statement is compiled by the
+:class:`_engine.Connection` for execution; calling the :meth:`_expression.ClauseElement.compile`
+method will not emit a warning unless the linting flag is supplied::
+
+    >>> from sqlalchemy.sql import FROM_LINTING
+    >>> print(q.statement.compile(linting=FROM_LINTING))
+    SAWarning: SELECT statement has a cartesian product between FROM element(s) "addresses" and FROM element "users".  Apply join condition(s) between each element to resolve.
+    SELECT users.id, users.name, users.fullname, users.nickname
+    FROM addresses, users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
+    WHERE addresses.email_address = :email_address_1
+
+:ticket:`4737`
+
+
 .. _change_result_14_core:
 
 New Result object
@@ -782,619 +969,639 @@ There are many reasons why the above assumptions do not hold:
 
 :ticket:`4710`
 
-New Features - ORM
-==================
+.. _change_4753:
 
-.. _change_4826:
+SELECT objects and derived FROM clauses allow for duplicate columns and column labels
+-------------------------------------------------------------------------------------
 
-Raiseload for Columns
----------------------
+This change allows that the :func:`_expression.select` construct now allows for duplicate
+column labels as well as duplicate column objects themselves, so that result
+tuples are organized and ordered in the identical way in that the columns were
+selected.  The ORM :class:`_query.Query` already works this way, so this change
+allows for greater cross-compatibility between the two, which is a key goal of
+the 2.0 transition::
 
-The "raiseload" feature, which raises :class:`.InvalidRequestError` when an
-unloaded attribute is accessed, is now available for column-oriented attributes
-using the :paramref:`.orm.defer.raiseload` parameter of :func:`.defer`. This
-works in the same manner as that of the :func:`.raiseload` option used by
-relationship loading::
+    >>> from sqlalchemy import column, select
+    >>> c1, c2, c3, c4 = column('c1'), column('c2'), column('c3'), column('c4')
+    >>> stmt = select([c1, c2, c3.label('c2'), c2, c4])
+    >>> print(stmt)
+    SELECT c1, c2, c3 AS c2, c2, c4
 
-    book = session.query(Book).options(defer(Book.summary, raiseload=True)).first()
+To support this change, the :class:`_expression.ColumnCollection` used by
+:class:`_expression.SelectBase` as well as for derived FROM clauses such as subqueries
+also support duplicate columns; this includes the new
+:attr:`_expression.SelectBase.selected_columns` attribute, the deprecated ``SelectBase.c``
+attribute, as well as the :attr:`_expression.FromClause.c` attribute seen on constructs
+such as :class:`.Subquery` and :class:`_expression.Alias`::
 
-    # would raise an exception
-    book.summary
+    >>> list(stmt.selected_columns)
+    [
+        <sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcca20; c1>,
+        <sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcc9e8; c2>,
+        <sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>,
+        <sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcc9e8; c2>,
+        <sqlalchemy.sql.elements.ColumnClause at 0x7fa540897048; c4>
+    ]
 
-To configure column-level raiseload on a mapping, the
-:paramref:`.deferred.raiseload` parameter of :func:`.deferred` may be used.  The
-:func:`.undefer` option may then be used at query time to eagerly load
-the attribute::
+    >>> print(stmt.subquery().select())
+    SELECT anon_1.c1, anon_1.c2, anon_1.c2, anon_1.c2, anon_1.c4
+    FROM (SELECT c1, c2, c3 AS c2, c2, c4) AS anon_1
 
-    class Book(Base):
-        __tablename__ = 'book'
+:class:`_expression.ColumnCollection` also allows access by integer index to support
+when the string "key" is ambiguous::
 
-        book_id = Column(Integer, primary_key=True)
-        title = Column(String(200), nullable=False)
-        summary = deferred(Column(String(2000)), raiseload=True)
-        excerpt = deferred(Column(Text), raiseload=True)
+    >>> stmt.selected_columns[2]
+    <sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>
 
-    book_w_excerpt = session.query(Book).options(undefer(Book.excerpt)).first()
+To suit the use of :class:`_expression.ColumnCollection` in objects such as
+:class:`_schema.Table` and :class:`.PrimaryKeyConstraint`, the old "deduplicating"
+behavior which is more critical for these objects is preserved in a new class
+:class:`.DedupeColumnCollection`.
 
-It was originally considered that the existing :func:`.raiseload` option that
-works for :func:`_orm.relationship` attributes be expanded to also support column-oriented
-attributes.    However, this would break the "wildcard" behavior of :func:`.raiseload`,
-which is documented as allowing one to prevent all relationships from loading::
+The change includes that the familiar warning ``"Column %r on table %r being
+replaced by %r, which has the same key.  Consider use_labels for select()
+statements."`` is **removed**; the :meth:`_expression.Select.apply_labels` is still
+available and is still used by the ORM for all SELECT operations, however it
+does not imply deduplication of column objects, although it does imply
+deduplication of implicitly generated labels::
 
-    session.query(Order).options(
-        joinedload(Order.items), raiseload('*'))
-
-Above, if we had expanded :func:`.raiseload` to accommodate for columns  as
-well, the wildcard would also prevent columns from loading and thus be  a
-backwards incompatible change; additionally, it's not clear if
-:func:`.raiseload` covered both column expressions and relationships, how one
-would achieve the  effect above of only blocking relationship loads, without
-new API being added.   So to keep things simple, the option for columns
-remains on :func:`.defer`:
+    >>> from sqlalchemy import table
+    >>> user = table('user', column('id'), column('name'))
+    >>> stmt = select([user.c.id, user.c.name, user.c.id]).apply_labels()
+    >>> print(stmt)
+    SELECT "user".id AS user_id, "user".name AS user_name, "user".id AS id_1
+    FROM "user"
 
-    :func:`.raiseload` - query option to raise for relationship loads
+Finally, the change makes it easier to create UNION and other
+:class:`_selectable.CompoundSelect` objects, by ensuring that the number and position
+of columns in a SELECT statement mirrors what was given, in a use case such
+as::
 
-    :paramref:`.orm.defer.raiseload` - query option to raise for column expression loads
+    >>> s1 = select([user, user.c.id])
+    >>> s2 = select([c1, c2, c3])
+    >>> from sqlalchemy import union
+    >>> u = union(s1, s2)
+    >>> print(u)
+    SELECT "user".id, "user".name, "user".id
+    FROM "user" UNION SELECT c1, c2, c3
 
 
-As part of this change, the behavior of "deferred" in conjunction with
-attribute expiration has changed.   Previously, when an object would be marked
-as expired, and then unexpired via the access of one of the expired attributes,
-attributes which were mapped as "deferred" at the mapper level would also load.
-This has been changed such that an attribute that is deferred in the mapping
-will never "unexpire", it only loads when accessed as part of the deferral
-loader.
 
-An attribute that is not mapped as "deferred", however was deferred at query
-time via the :func:`.defer` option, will be reset when the object or attribute
-is expired; that is, the deferred option is removed. This is the same behavior
-as was present previously.
+:ticket:`4753`
 
 
-.. seealso::
 
-    :ref:`deferred_raiseload`
+.. _change_4449:
 
-:ticket:`4826`
+Improved column labeling for simple column expressions using CAST or similar
+----------------------------------------------------------------------------
 
-.. _change_5263:
+A user pointed out that the PostgreSQL database has a convenient behavior when
+using functions like CAST against a named column, in that the result column name
+is named the same as the inner expression::
 
-ORM Batch inserts with psycopg2 now batch statements with RETURNING in most cases
----------------------------------------------------------------------------------
+    test=> SELECT CAST(data AS VARCHAR) FROM foo;
 
-The change in :ref:`change_5401` adds support for "executemany" + "RETURNING"
-at the same time in Core, which is now enabled for the psycopg2 dialect
-by default using the psycopg2 ``execute_values()`` extension.   The ORM flush
-process now makes use of this feature such that the retrieval of newly generated
-primary key values and server defaults can be achieved while not losing the
-performance benefits of being able to batch INSERT statements together.  Additionally,
-psycopg2's ``execute_values()`` extension itself provides a five-fold performance
-improvement over psycopg2's default "executemany" implementation, by rewriting
-an INSERT statement to include many "VALUES" expressions all in one statement
-rather than invoking the same statement repeatedly, as psycopg2 lacks the ability
-to PREPARE the statement ahead of time as would normally be expected for this
-approach to be performant.
+    data
+    ------
+     5
+    (1 row)
 
-SQLAlchemy includes a :ref:`performance suite <examples_performance>` within
-its examples, where we can compare the times generated for the "batch_inserts"
-runner against 1.3 and 1.4, revealing a 3x-5x speedup for most flavors
-of batch insert::
+This allows one to apply CAST to table columns while not losing the column
+name (above using the name ``"data"``) in the result row.    Compare to
+databases such as MySQL/MariaDB, as well as most others, where the column
+name is taken from the full SQL expression and is not very portable::
 
-    # 1.3
-    $ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
-    test_flush_no_pk : (100000 iterations); total time 14.051527 sec
-    test_bulk_save_return_pks : (100000 iterations); total time 15.002470 sec
-    test_flush_pk_given : (100000 iterations); total time 7.863680 sec
-    test_bulk_save : (100000 iterations); total time 6.780378 sec
-    test_bulk_insert_mappings :  (100000 iterations); total time 5.363070 sec
-    test_core_insert : (100000 iterations); total time 5.362647 sec
+    MariaDB [test]> SELECT CAST(data AS CHAR) FROM foo;
+    +--------------------+
+    | CAST(data AS CHAR) |
+    +--------------------+
+    | 5                  |
+    +--------------------+
+    1 row in set (0.003 sec)
 
-    # 1.4 with enhancement
-    $ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
-    test_flush_no_pk : (100000 iterations); total time 3.820807 sec
-    test_bulk_save_return_pks : (100000 iterations); total time 3.176378 sec
-    test_flush_pk_given : (100000 iterations); total time 4.037789 sec
-    test_bulk_save : (100000 iterations); total time 2.604446 sec
-    test_bulk_insert_mappings : (100000 iterations); total time 1.204897 sec
-    test_core_insert : (100000 iterations); total time 0.958976 sec
 
-Note that the ``execute_values()`` extension modifies the INSERT statement in the psycopg2
-layer, **after** it's been logged by SQLAlchemy.  So with SQL logging, one will see the
-parameter sets batched together, but the joining of multiple "values" will not be visible
-on the application side::
+In SQLAlchemy Core expressions, we never deal with a raw generated name like
+the above, as SQLAlchemy applies auto-labeling to expressions like these, which
+are up until now always a so-called "anonymous" expression::
 
-    2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (%(data)s) RETURNING a.id
-    2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine [generated in 0.00698s] ({'data': 'data 1'}, {'data': 'data 2'}, {'data': 'data 3'}, {'data': 'data 4'}, {'data': 'data 5'}, {'data': 'data 6'}, {'data': 'data 7'}, {'data': 'data 8'}  ... displaying 10 of 4999 total bound parameter sets ...  {'data': 'data 4998'}, {'data': 'data 4999'})
-    2020-06-27 19:08:18,254 INFO sqlalchemy.engine.Engine COMMIT
+    >>> print(select([cast(foo.c.data, String)]))
+    SELECT CAST(foo.data AS VARCHAR) AS anon_1     # old behavior
+    FROM foo
 
-The ultimate INSERT statement can be seen by enabling statement logging on the PostgreSQL side::
+These anonymous expressions were necessary as SQLAlchemy's
+:class:`_engine.ResultProxy` made heavy use of result column names in order to match
+up datatypes, such as the :class:`.String` datatype which used to have
+result-row-processing behavior, to the correct column, so most importantly the
+names had to be both easy to determine in a database-agnostic manner as well as
+unique in all cases.    In SQLAlchemy 1.0 as part of :ticket:`918`, this
+reliance on named columns in result rows (specifically the
+``cursor.description`` element of the PEP-249 cursor) was scaled back to not be
+necessary for most Core SELECT constructs; in release 1.4, the system overall
+is becoming more comfortable with SELECT statements that have duplicate column
+or label names such as in :ref:`change_4753`.  So we now emulate PostgreSQL's
+reasonable behavior for simple modifications to a single column, most
+prominently with CAST::
 
-    2020-06-27 19:08:18.169 EDT [26960] LOG:  statement: INSERT INTO a (data)
-    VALUES ('data 1'),('data 2'),('data 3'),('data 4'),('data 5'),('data 6'),('data
-    7'),('data 8'),('data 9'),('data 10'),('data 11'),('data 12'),
-    ... ('data 999'),('data 1000') RETURNING a.id
+    >>> print(select([cast(foo.c.data, String)]))
+    SELECT CAST(foo.data AS VARCHAR) AS data
+    FROM foo
 
-    2020-06-27 19:08:18.175 EDT
-    [26960] LOG:  statement: INSERT INTO a (data) VALUES ('data 1001'),('data
-    1002'),('data 1003'),('data 1004'),('data 1005 '),('data 1006'),('data
-    1007'),('data 1008'),('data 1009'),('data 1010'),('data 1011'), ...
+For CAST against expressions that don't have a name, the previous logic is used
+to generate the usual "anonymous" labels::
 
-The feature batches rows into groups of 1000 by default which can be affected
-using the ``executemany_values_page_size`` argument documented at
-:ref:`psycopg2_executemany_mode`.
+    >>> print(select([cast('hi there,' + foo.c.data, String)]))
+    SELECT CAST(:data_1 + foo.data AS VARCHAR) AS anon_1
+    FROM foo
 
-:ticket:`5263`
+A :func:`.cast` against a :class:`.Label`, despite having to omit the label
+expression as these don't render inside of a CAST, will nonetheless make use of
+the given name::
 
+    >>> print(select([cast(('hi there,' + foo.c.data).label('hello_data'), String)]))
+    SELECT CAST(:data_1 + foo.data AS VARCHAR) AS hello_data
+    FROM foo
 
-.. _change_orm_update_returning_14:
+And of course as was always the case, :class:`.Label` can be applied to the
+expression on the outside to apply an "AS <name>" label directly::
 
-ORM Bulk Update and Delete use RETURNING for "fetch" strategy when available
-----------------------------------------------------------------------------
+    >>> print(select([cast(('hi there,' + foo.c.data), String).label('hello_data')]))
+    SELECT CAST(:data_1 + foo.data AS VARCHAR) AS hello_data
+    FROM foo
 
-An ORM bulk update or delete that uses the "fetch" strategy::
 
-    sess.query(User).filter(User.age > 29).update(
-        {"age": User.age - 10}, synchronize_session="fetch"
-    )
+:ticket:`4449`
 
-Will now use RETURNING if the backend database supports it; this currently
-includes PostgreSQL and SQL Server (the Oracle dialect does not support RETURNING
-of multiple rows)::
+.. _change_4808:
 
-    UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s RETURNING users.id
-    [generated in 0.00060s] {'age_int_1': 10, 'age_int_2': 29}
-    Col ('id',)
-    Row (2,)
-    Row (4,)
+New "post compile" bound parameters used for LIMIT/OFFSET in Oracle, SQL Server
+-------------------------------------------------------------------------------
 
-For backends that do not support RETURNING of multiple rows, the previous approach
-of emitting SELECT for the primary keys beforehand is still used::
+A major goal of the 1.4 series is to establish that all Core SQL constructs
+are completely cacheable, meaning that a particular :class:`.Compiled`
+structure will produce an identical SQL string regardless of any SQL parameters
+used with it, which notably includes those used to specify the LIMIT and
+OFFSET values, typically used for pagination and "top N" style results.
 
-    SELECT users.id FROM users WHERE users.age_int > %(age_int_1)s
-    [generated in 0.00043s] {'age_int_1': 29}
-    Col ('id',)
-    Row (2,)
-    Row (4,)
-    UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s
-    [generated in 0.00102s] {'age_int_1': 10, 'age_int_2': 29}
+While SQLAlchemy has used bound parameters for LIMIT/OFFSET schemes for many
+years, a few outliers remained where such parameters were not allowed, including
+a SQL Server "TOP N" statement, such as::
 
-One of the intricate challenges of this change is to support cases such as the
-horizontal sharding extension, where a single bulk update or delete may be
-multiplexed among backends some of which support RETURNING and some don't.   The
-new 1.4 execution archiecture supports this case so that the "fetch" strategy
-can be left intact with a graceful degrade to using a SELECT, rather than having
-to add a new "returning" strategy that would not be backend-agnostic.
+    SELECT TOP 5 mytable.id, mytable.data FROM mytable
 
-As part of this change, the "fetch" strategy is also made much more efficient
-in that it will no longer expire the objects located which match the rows,
-for Python expressions used in the SET clause which can be evaluated in
-Python; these are instead assigned
-directly onto the object in the same way as the "evaluate" strategy.  Only
-for SQL expressions that can't be evaluated does it fall back to expiring
-the attributes.   The "evaluate" strategy has also been enhanced to fall back
-to "expire" for a value that cannot be evaluated.
+as well as with Oracle, where the FIRST_ROWS() hint (which SQLAlchemy will
+use if the ``optimize_limits=True`` parameter is passed to
+:func:`_sa.create_engine` with an Oracle URL) does not allow them,
+but also that using bound parameters with ROWNUM comparisons has been reported
+as producing slower query plans::
 
+    SELECT anon_1.id, anon_1.data FROM (
+        SELECT /*+ FIRST_ROWS(5) */
+        anon_2.id AS id,
+        anon_2.data AS data,
+        ROWNUM AS ora_rn FROM (
+            SELECT mytable.id, mytable.data FROM mytable
+        ) anon_2
+        WHERE ROWNUM <= :param_1
+    ) anon_1 WHERE ora_rn > :param_2
 
-Behavioral Changes - ORM
-========================
+In order to allow for all statements to be unconditionally cacheable at the
+compilation level, a new form of bound parameter called a "post compile"
+parameter has been added, which makes use of the same mechanism as that
+of "expanding IN parameters".  This is a :func:`.bindparam` that behaves
+identically to any other bound parameter except that parameter value will
+be rendered literally into the SQL string before sending it to the DBAPI
+``cursor.execute()`` method.   The new parameter is used internally by the
+SQL Server and Oracle dialects, so that the drivers receive the literal
+rendered value but the rest of SQLAlchemy can still consider this as a
+bound parameter.   The above two statements when stringified using
+``str(statement.compile(dialect=<dialect>))`` now look like::
 
-.. _change_4710_orm:
+    SELECT TOP [POSTCOMPILE_param_1] mytable.id, mytable.data FROM mytable
 
-The "KeyedTuple" object returned by Query is replaced by Row
--------------------------------------------------------------
+and::
 
-As discussed at :ref:`change_4710_core`, the Core :class:`.RowProxy` object
-is now replaced by a class called :class:`.Row`.    The base :class:`.Row`
-object now behaves more fully like a named tuple, and as such it is now
-used as the basis for tuple-like results returned by the :class:`_query.Query`
-object, rather than the previous "KeyedTuple" class.
+    SELECT anon_1.id, anon_1.data FROM (
+        SELECT /*+ FIRST_ROWS([POSTCOMPILE__ora_frow_1]) */
+        anon_2.id AS id,
+        anon_2.data AS data,
+        ROWNUM AS ora_rn FROM (
+            SELECT mytable.id, mytable.data FROM mytable
+        ) anon_2
+        WHERE ROWNUM <= [POSTCOMPILE_param_1]
+    ) anon_1 WHERE ora_rn > [POSTCOMPILE_param_2]
 
-The rationale is so that by SQLAlchemy 2.0, both Core and ORM SELECT statements
-will return result rows using the same :class:`.Row` object which behaves  like
-a named tuple.  Dictionary-like functionality is available from :class:`.Row`
-via the :attr:`.Row._mapping` attribute.   In the interim, Core result sets
-will make use of a :class:`.Row` subclass :class:`.LegacyRow` which maintains
-the previous dict/tuple hybrid behavior for backwards compatibility while the
-:class:`.Row` class will be used directly for ORM tuple results returned
-by the :class:`_query.Query` object.
-
-Effort has been made to get most of the featureset of :class:`.Row` to be
-available within the ORM, meaning that access by string name as well
-as entity / column should work::
-
-    row = s.query(User, Address).join(User.addresses).first()
+The ``[POSTCOMPILE_<param>]`` format is also what is seen when an
+"expanding IN" is used.
 
-    row._mapping[User]  # same as row[0]
-    row._mapping[Address]  # same as row[1]
-    row._mapping["User"]  # same as row[0]
-    row._mapping["Address"]  # same as row[1]
+When viewing the SQL logging output, the final form of the statement will
+be seen::
 
-    u1 = aliased(User)
-    row = s.query(u1).only_return_tuples(True).first()
-    row._mapping[u1]  # same as row[0]
+    SELECT anon_1.id, anon_1.data FROM (
+        SELECT /*+ FIRST_ROWS(5) */
+        anon_2.id AS id,
+        anon_2.data AS data,
+        ROWNUM AS ora_rn FROM (
+            SELECT mytable.id AS id, mytable.data AS data FROM mytable
+        ) anon_2
+        WHERE ROWNUM <= 8
+    ) anon_1 WHERE ora_rn > 3
 
 
-    row = (
-        s.query(User.id, Address.email_address)
-        .join(User.addresses)
-        .first()
-    )
+The "post compile parameter" feature is exposed as public API through the
+:paramref:`.bindparam.literal_execute` parameter, however is currently not
+intended for general use.   The literal values are rendered using the
+:meth:`.TypeEngine.literal_processor` of the underlying datatype, which in
+SQLAlchemy has **extremely limited** scope, supporting only integers and simple
+string values.
 
-    row._mapping[User.id]  # same as row[0]
-    row._mapping["id"]  # same as row[0]
-    row._mapping[users.c.id]  # same as row[0]
+:ticket:`4808`
 
-.. seealso::
+.. _change_4712:
 
-    :ref:`change_4710_core`
+Connection-level transactions can now be inactive based on subtransaction
+-------------------------------------------------------------------------
 
-:ticket:`4710`.
+A :class:`_engine.Connection` now includes the behavior where a :class:`.Transaction`
+can be made inactive due to a rollback on an inner transaction, however the
+:class:`.Transaction` will not clear until it is itself rolled back.
 
-.. _change_5074:
+This is essentially a new error condition which will disallow statement
+executions to proceed on a :class:`_engine.Connection` if an inner "sub" transaction
+has been rolled back.  The behavior works very similarly to that of the
+ORM :class:`.Session`, where if an outer transaction has been begun, it needs
+to be rolled back to clear the invalid transaction; this behavior is described
+in :ref:`faq_session_rollback`.
 
-Session does not immediately create a new SessionTransaction object
-----------------------------------------------------------------------------
+While the :class:`_engine.Connection` has had a less strict behavioral pattern than
+the :class:`.Session`, this change was made as it helps to identify when
+a subtransaction has rolled back the DBAPI transaction, however the external
+code isn't aware of this and attempts to continue proceeding, which in fact
+runs operations on a new transaction.   The "test harness" pattern described
+at :ref:`session_external_transaction` is the common place for this to occur.
 
-The :class:`.Session` object's default behavior of ``autocommit=False``
-historically has meant that there is always a :class:`.SessionTransaction`
-object in play, associated with the :class:`.Session` via the
-:attr:`.Session.transaction` attribute.   When the given
-:class:`.SessionTransaction` was complete, due to a commit, rollback, or close,
-it was immediately replaced with a new one.  The :class:`.SessionTransaction`
-by itself does not imply the usage of any connection-oriented resources, so
-this long-standing behavior has a particular elegance to it in that the state
-of :attr:`.Session.transaction` is always predictable as non-None.
+The "subtransaction" feature of Core and ORM is itself deprecated and will
+no longer be present in version 2.0.   As a result, this new error condition
+is itself temporary as it will no longer apply once subtransactions are removed.
 
-However, as part of the initiative in :ticket:`5056` to greatly reduce
-reference cycles, this assumption means that calling upon
-:meth:`.Session.close` results in a :class:`.Session` object that still has
-reference cycles and is more expensive to clean up, not to mention that there
-is a small overhead in constructing the :class:`.SessionTransaction`
-object, which meant that there would be unnecessary overhead created
-for a :class:`.Session` that for example invoked :meth:`.Session.commit`
-and then :meth:`.Session.close`.
+In order to work with the 2.0 style behavior that does not include
+subtransactions, use the :paramref:`_sa.create_engine.future` parameter
+on :func:`_sa.create_engine`.
 
-As such, it was decided that :meth:`.Session.close` should leave the internal
-state of ``self.transaction``, now referred to internally as
-``self._transaction``, as None, and that a new :class:`.SessionTransaction`
-should only be created when needed.  For consistency and code coverage, this
-behavior was also expanded to include all the points at which "autobegin" is
-expected, not just when :meth:`.Session.close` were called.
+The error message is described in the errors page at :ref:`error_8s2a`.
 
-In particular, this causes a behavioral change for applications which
-subscribe to the :meth:`.SessionEvents.after_transaction_create` event hook;
-previously, this event would be emitted when the :class:`.Session` were  first
-constructed, as well as for most actions that closed the previous transaction
-and would emit :meth:`.SessionEvents.after_transaction_end`.  The new behavior
-is that :meth:`.SessionEvents.after_transaction_create` is emitted on demand,
-when the :class:`.Session` has not yet created a  new
-:class:`.SessionTransaction` object and mapped objects are associated with the
-:class:`.Session` through methods like :meth:`.Session.add` and
-:meth:`.Session.delete`, when  the :attr:`.Session.transaction` attribute is
-called upon, when the :meth:`.Session.flush` method has tasks to complete, etc.
 
-Besides the change in when the :meth:`.SessionEvents.after_transaction_create`
-event is emitted, the change should have no other user-visible impact on the
-:class:`.Session` object's behavior; the :class:`.Session` will continue to have
-the behavior that it remains usable for new operations after :meth:`.Session.close`
-is called, and the sequencing of how the :class:`.Session` interacts with the
-:class:`_engine.Engine` and the database itself should also remain unaffected, since
-these operations were already operating in an on-demand fashion.
 
-:ticket:`5074`
+New Features - ORM
+==================
 
-.. _change_5237_14:
+.. _change_4826:
 
-Viewonly relationships don't synchronize backrefs
--------------------------------------------------
+Raiseload for Columns
+---------------------
 
-In :ticket:`5149` in 1.3.14, SQLAlchemy began emitting a warning when the
-:paramref:`_orm.relationship.backref` or :paramref:`_orm.relationship.back_populates`
-keywords would be used at the same time as the :paramref:`_orm.relationship.viewonly`
-flag on the target relationship.  This was because a "viewonly" relationship does
-not actually persist changes made to it, which could cause some misleading
-behaviors to occur.  However, in :ticket:`5237`, we sought to refine this
-behavior as there are legitimate use cases to have backrefs set up on
-viewonly relationships, including that back populates attributes are used
-in some cases by the relationship lazy loaders to determine that an additional
-eager load in the other direction is not necessary, as well as that back
-populates can be used for mapper introspection and that :func:`_orm.backref`
-can be a convenient way to set up bi-directional relationships.
+The "raiseload" feature, which raises :class:`.InvalidRequestError` when an
+unloaded attribute is accessed, is now available for column-oriented attributes
+using the :paramref:`.orm.defer.raiseload` parameter of :func:`.defer`. This
+works in the same manner as that of the :func:`.raiseload` option used by
+relationship loading::
 
-The solution then was to make the "mutation" that occurs from a backref
-an optional thing, using the :paramref:`_orm.relationship.sync_backref`
-flag.  In 1.4 the value of :paramref:`_orm.relationship.sync_backref` defaults
-to False for a relationship target that also sets :paramref:`_orm.relationship.viewonly`.
-This indicates that any changes made to a relationship with
-viewonly will not impact the state of the other side or of the :class:`_orm.Session`
-in any way::
+    book = session.query(Book).options(defer(Book.summary, raiseload=True)).first()
 
+    # would raise an exception
+    book.summary
 
-    class User(Base):
-        # ...
+To configure column-level raiseload on a mapping, the
+:paramref:`.deferred.raiseload` parameter of :func:`.deferred` may be used.  The
+:func:`.undefer` option may then be used at query time to eagerly load
+the attribute::
 
-        addresses = relationship(Address, backref=backref("user", viewonly=True))
+    class Book(Base):
+        __tablename__ = 'book'
 
-    class Address(Base):
-        # ...
+        book_id = Column(Integer, primary_key=True)
+        title = Column(String(200), nullable=False)
+        summary = deferred(Column(String(2000)), raiseload=True)
+        excerpt = deferred(Column(Text), raiseload=True)
 
+    book_w_excerpt = session.query(Book).options(undefer(Book.excerpt)).first()
 
-    u1 = session.query(User).filter_by(name="x").first()
+It was originally considered that the existing :func:`.raiseload` option that
+works for :func:`_orm.relationship` attributes be expanded to also support column-oriented
+attributes.    However, this would break the "wildcard" behavior of :func:`.raiseload`,
+which is documented as allowing one to prevent all relationships from loading::
 
-    a1 = Address()
-    a1.user = u1
+    session.query(Order).options(
+        joinedload(Order.items), raiseload('*'))
 
-Above, the ``a1`` object will **not** be added to the ``u1.addresses``
-collection, nor will the ``a1`` object be added to the session.  Previously,
-both of these things would be true.   The warning that
-:paramref:`.relationship.sync_backref` should be set to ``False`` when
-:paramref:`.relationship.viewonly` is ``False`` is no longer emitted as this is
-now the default behavior.
+Above, if we had expanded :func:`.raiseload` to accommodate for columns  as
+well, the wildcard would also prevent columns from loading and thus be  a
+backwards incompatible change; additionally, it's not clear if
+:func:`.raiseload` covered both column expressions and relationships, how one
+would achieve the  effect above of only blocking relationship loads, without
+new API being added.   So to keep things simple, the option for columns
+remains on :func:`.defer`:
 
-:ticket:`5237`
+    :func:`.raiseload` - query option to raise for relationship loads
 
-.. _change_1763:
+    :paramref:`.orm.defer.raiseload` - query option to raise for column expression loads
 
-Eager loaders emit during unexpire operations
----------------------------------------------
 
-A long sought behavior was that when an expired object is accessed, configured
-eager loaders will run in order to eagerly load relationships on the expired
-object when the object is refreshed or otherwise unexpired.   This behavior has
-now been added, so that joinedloaders will add inline JOINs as usual, and
-selectin/subquery loaders will run an "immediateload" operation for a given
-relationship, when an expired object is unexpired or an object is refreshed::
+As part of this change, the behavior of "deferred" in conjunction with
+attribute expiration has changed.   Previously, when an object would be marked
+as expired, and then unexpired via the access of one of the expired attributes,
+attributes which were mapped as "deferred" at the mapper level would also load.
+This has been changed such that an attribute that is deferred in the mapping
+will never "unexpire", it only loads when accessed as part of the deferral
+loader.
 
-    >>> a1 = session.query(A).options(joinedload(A.bs)).first()
-    >>> a1.data = 'new data'
-    >>> session.commit()
+An attribute that is not mapped as "deferred", however was deferred at query
+time via the :func:`.defer` option, will be reset when the object or attribute
+is expired; that is, the deferred option is removed. This is the same behavior
+as was present previously.
 
-Above, the ``A`` object was loaded with a ``joinedload()`` option associated
-with it in order to eagerly load the ``bs`` collection.    After the
-``session.commit()``, the state of the object is expired.  Upon accessing
-the ``.data`` column attribute, the object is refreshed and this will now
-include the joinedload operation as well::
 
-    >>> a1.data
-    SELECT a.id AS a_id, a.data AS a_data, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
-    FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id
-    WHERE a.id = ?
+.. seealso::
 
-The behavior applies both to loader strategies applied to the
-:func:`_orm.relationship` directly, as well as with options used with
-:meth:`_query.Query.options`, provided that the object was originally loaded by that
-query.
+    :ref:`deferred_raiseload`
 
-For the "secondary" eager loaders "selectinload" and "subqueryload", the SQL
-strategy for these loaders is not necessary in order to eagerly load attributes
-on a single object; so they will instead invoke the "immediateload" strategy in
-a refresh scenario, which resembles the query emitted by "lazyload", emitted as
-an additional query::
+:ticket:`4826`
 
-    >>> a1 = session.query(A).options(selectinload(A.bs)).first()
-    >>> a1.data = 'new data'
-    >>> session.commit()
-    >>> a1.data
-    SELECT a.id AS a_id, a.data AS a_data
-    FROM a
-    WHERE a.id = ?
-    (1,)
-    SELECT b.id AS b_id, b.a_id AS b_a_id
-    FROM b
-    WHERE ? = b.a_id
-    (1,)
+.. _change_5263:
 
-Note that a loader option does not apply to an object that was introduced
-into the :class:`.Session` in a different way.  That is, if the ``a1`` object
-were just persisted in this :class:`.Session`, or was loaded with a different
-query before the eager option had been applied, then the object doesn't have
-an eager load option associated with it.  This is not a new concept, however
-users who are looking for the eagerload on refresh behavior may find this
-to be more noticeable.
+ORM Batch inserts with psycopg2 now batch statements with RETURNING in most cases
+---------------------------------------------------------------------------------
 
-:ticket:`1763`
+The change in :ref:`change_5401` adds support for "executemany" + "RETURNING"
+at the same time in Core, which is now enabled for the psycopg2 dialect
+by default using the psycopg2 ``execute_values()`` extension.   The ORM flush
+process now makes use of this feature such that the retrieval of newly generated
+primary key values and server defaults can be achieved while not losing the
+performance benefits of being able to batch INSERT statements together.  Additionally,
+psycopg2's ``execute_values()`` extension itself provides a five-fold performance
+improvement over psycopg2's default "executemany" implementation, by rewriting
+an INSERT statement to include many "VALUES" expressions all in one statement
+rather than invoking the same statement repeatedly, as psycopg2 lacks the ability
+to PREPARE the statement ahead of time as would normally be expected for this
+approach to be performant.
 
-.. _change_4519:
+SQLAlchemy includes a :ref:`performance suite <examples_performance>` within
+its examples, where we can compare the times generated for the "batch_inserts"
+runner against 1.3 and 1.4, revealing a 3x-5x speedup for most flavors
+of batch insert::
 
-Accessing an uninitialized collection attribute on a transient object no longer mutates __dict__
--------------------------------------------------------------------------------------------------
+    # 1.3
+    $ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
+    test_flush_no_pk : (100000 iterations); total time 14.051527 sec
+    test_bulk_save_return_pks : (100000 iterations); total time 15.002470 sec
+    test_flush_pk_given : (100000 iterations); total time 7.863680 sec
+    test_bulk_save : (100000 iterations); total time 6.780378 sec
+    test_bulk_insert_mappings :  (100000 iterations); total time 5.363070 sec
+    test_core_insert : (100000 iterations); total time 5.362647 sec
 
-It has always been SQLAlchemy's behavior that accessing mapped attributes on a
-newly created object returns an implicitly generated value, rather than raising
-``AttributeError``, such as ``None`` for scalar attributes or ``[]`` for a
-list-holding relationship::
+    # 1.4 with enhancement
+    $ python -m examples.performance bulk_inserts --dburl postgresql://scott:tiger@localhost/test
+    test_flush_no_pk : (100000 iterations); total time 3.820807 sec
+    test_bulk_save_return_pks : (100000 iterations); total time 3.176378 sec
+    test_flush_pk_given : (100000 iterations); total time 4.037789 sec
+    test_bulk_save : (100000 iterations); total time 2.604446 sec
+    test_bulk_insert_mappings : (100000 iterations); total time 1.204897 sec
+    test_core_insert : (100000 iterations); total time 0.958976 sec
 
-    >>> u1 = User()
-    >>> u1.name
-    None
-    >>> u1.addresses
-    []
+Note that the ``execute_values()`` extension modifies the INSERT statement in the psycopg2
+layer, **after** it's been logged by SQLAlchemy.  So with SQL logging, one will see the
+parameter sets batched together, but the joining of multiple "values" will not be visible
+on the application side::
 
-The rationale for the above behavior was originally to make ORM objects easier
-to work with.  Since an ORM object represents an empty row when first created
-without any state, it is intuitive that its un-accessed attributes would
-resolve to ``None`` (or SQL NULL) for scalars and to empty collections for
-relationships.   In particular, it makes possible an extremely common pattern
-of being able to mutate the new collection without manually creating and
-assigning an empty collection first::
+    2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine INSERT INTO a (data) VALUES (%(data)s) RETURNING a.id
+    2020-06-27 19:08:18,166 INFO sqlalchemy.engine.Engine [generated in 0.00698s] ({'data': 'data 1'}, {'data': 'data 2'}, {'data': 'data 3'}, {'data': 'data 4'}, {'data': 'data 5'}, {'data': 'data 6'}, {'data': 'data 7'}, {'data': 'data 8'}  ... displaying 10 of 4999 total bound parameter sets ...  {'data': 'data 4998'}, {'data': 'data 4999'})
+    2020-06-27 19:08:18,254 INFO sqlalchemy.engine.Engine COMMIT
 
-    >>> u1 = User()
-    >>> u1.addresses.append(Address())  # no need to assign u1.addresses = []
+The ultimate INSERT statement can be seen by enabling statement logging on the PostgreSQL side::
 
-Up until version 1.0 of SQLAlchemy, the behavior of this initialization  system
-for both scalar attributes as well as collections would be that the ``None`` or
-empty collection would be *populated* into the object's  state, e.g.
-``__dict__``.  This meant that the following two operations were equivalent::
+    2020-06-27 19:08:18.169 EDT [26960] LOG:  statement: INSERT INTO a (data)
+    VALUES ('data 1'),('data 2'),('data 3'),('data 4'),('data 5'),('data 6'),('data
+    7'),('data 8'),('data 9'),('data 10'),('data 11'),('data 12'),
+    ... ('data 999'),('data 1000') RETURNING a.id
 
-    >>> u1 = User()
-    >>> u1.name = None  # explicit assignment
+    2020-06-27 19:08:18.175 EDT
+    [26960] LOG:  statement: INSERT INTO a (data) VALUES ('data 1001'),('data
+    1002'),('data 1003'),('data 1004'),('data 1005 '),('data 1006'),('data
+    1007'),('data 1008'),('data 1009'),('data 1010'),('data 1011'), ...
 
-    >>> u2 = User()
-    >>> u2.name  # implicit assignment just by accessing it
-    None
+The feature batches rows into groups of 1000 by default which can be affected
+using the ``executemany_values_page_size`` argument documented at
+:ref:`psycopg2_executemany_mode`.
 
-Where above, both ``u1`` and ``u2`` would have the value ``None`` populated
-in the value of the ``name`` attribute.  Since this is a SQL NULL, the ORM
-would skip including these values within an INSERT so that SQL-level defaults
-take place, if any, else the value defaults to NULL on the database side.
+:ticket:`5263`
 
-In version 1.0 as part of :ref:`migration_3061`, this behavior was refined so
-that the ``None`` value was no longer populated into ``__dict__``, only
-returned.   Besides removing the mutating side effect of a getter operation,
-this change also made it possible to set columns that did have server defaults
-to the value NULL by actually assigning ``None``, which was now distinguished
-from just reading it.
 
-The change however did not accommodate for collections, where returning an
-empty collection that is not assigned meant that this mutable collection would
-be different each time and also would not be able to correctly accommodate for
-mutating operations (e.g. append, add, etc.) called upon it.    While the
-behavior continued to generally not get in anyone's way, an edge case was
-eventually identified in :ticket:`4519` where this empty collection could be
-harmful, which is when the object is merged into a session::
+.. _change_orm_update_returning_14:
 
-    >>> u1 = User(id=1)  # create an empty User to merge with id=1 in the database
-    >>> merged1 = session.merge(u1)  # value of merged1.addresses is unchanged from that of the DB
+ORM Bulk Update and Delete use RETURNING for "fetch" strategy when available
+----------------------------------------------------------------------------
 
-    >>> u2 = User(id=2) # create an empty User to merge with id=2 in the database
-    >>> u2.addresses
-    []
-    >>> merged2 = session.merge(u2)  # value of merged2.addresses has been emptied in the DB
+An ORM bulk update or delete that uses the "fetch" strategy::
 
-Above, the ``.addresses`` collection on ``merged1`` will contain all the
-``Address()`` objects that were already in the database.   ``merged2`` will
-not; because it has an empty list implicitly assigned, the ``.addresses``
-collection will be erased.   This is an example of where this mutating side
-effect can actually mutate the database itself.
+    sess.query(User).filter(User.age > 29).update(
+        {"age": User.age - 10}, synchronize_session="fetch"
+    )
 
-While it was considered that perhaps the attribute system should begin using
-strict "plain Python" behavior, raising ``AttributeError`` in all cases for
-non-existent attributes on non-persistent objects and requiring that  all
-collections be explicitly assigned, such a change would likely be too extreme
-for the vast number of applications that have relied upon this  behavior for
-many years, leading to a complex rollout / backwards compatibility problem as
-well as the likelihood that workarounds to restore the old behavior would
-become prevalent, thus rendering the whole change ineffective in any case.
+Will now use RETURNING if the backend database supports it; this currently
+includes PostgreSQL and SQL Server (the Oracle dialect does not support RETURNING
+of multiple rows)::
 
-The change then is to keep the default producing behavior, but to finally make
-the non-mutating behavior of scalars a reality for collections as well, via the
-addition of additional mechanics in the collection system.  When accessing the
-empty attribute, the new collection is created and associated with the state,
-however is not added to ``__dict__`` until it is actually mutated::
+    UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s RETURNING users.id
+    [generated in 0.00060s] {'age_int_1': 10, 'age_int_2': 29}
+    Col ('id',)
+    Row (2,)
+    Row (4,)
 
-    >>> u1 = User()
-    >>> l1 = u1.addresses  # new list is created, associated with the state
-    >>> assert u1.addresses is l1  # you get the same list each time you access it
-    >>> assert "addresses" not in u1.__dict__  # but it won't go into __dict__ until it's mutated
-    >>> from sqlalchemy import inspect
-    >>> inspect(u1).attrs.addresses.history
-    History(added=None, unchanged=None, deleted=None)
+For backends that do not support RETURNING of multiple rows, the previous approach
+of emitting SELECT for the primary keys beforehand is still used::
 
-When the list is changed, then it becomes part of the tracked changes to
-be persisted to the database::
+    SELECT users.id FROM users WHERE users.age_int > %(age_int_1)s
+    [generated in 0.00043s] {'age_int_1': 29}
+    Col ('id',)
+    Row (2,)
+    Row (4,)
+    UPDATE users SET age_int=(users.age_int - %(age_int_1)s) WHERE users.age_int > %(age_int_2)s
+    [generated in 0.00102s] {'age_int_1': 10, 'age_int_2': 29}
 
-    >>> l1.append(Address())
-    >>> assert "addresses" in u1.__dict__
-    >>> inspect(u1).attrs.addresses.history
-    History(added=[<__main__.Address object at 0x7f49b725eda0>], unchanged=[], deleted=[])
+One of the intricate challenges of this change is to support cases such as the
+horizontal sharding extension, where a single bulk update or delete may be
+multiplexed among backends some of which support RETURNING and some don't.   The
+new 1.4 execution archiecture supports this case so that the "fetch" strategy
+can be left intact with a graceful degrade to using a SELECT, rather than having
+to add a new "returning" strategy that would not be backend-agnostic.
 
-This change is expected to have *nearly* no impact on existing applications
-in any way, except that it has been observed that some applications may be
-relying upon the implicit assignment of this collection, such as to assert that
-the object contains certain values based on its ``__dict__``::
+As part of this change, the "fetch" strategy is also made much more efficient
+in that it will no longer expire the objects located which match the rows,
+for Python expressions used in the SET clause which can be evaluated in
+Python; these are instead assigned
+directly onto the object in the same way as the "evaluate" strategy.  Only
+for SQL expressions that can't be evaluated does it fall back to expiring
+the attributes.   The "evaluate" strategy has also been enhanced to fall back
+to "expire" for a value that cannot be evaluated.
 
-    >>> u1 = User()
-    >>> u1.addresses
-    []
-    # this will now fail, would pass before
-    >>> assert {k: v for k, v in u1.__dict__.items() if not k.startswith("_")} == {"addresses": []}
 
-or to ensure that the collection won't require a lazy load to proceed, the
-(admittedly awkward) code below will now also fail::
+Behavioral Changes - ORM
+========================
 
-    >>> u1 = User()
-    >>> u1.addresses
-    []
-    >>> s.add(u1)
-    >>> s.flush()
-    >>> s.close()
-    >>> u1.addresses  # <-- will fail, .addresses is not loaded and object is detached
+.. _change_4710_orm:
 
-Applications that rely upon the implicit mutating behavior of collections will
-need to be changed so that they assign the desired collection explicitly::
+The "KeyedTuple" object returned by Query is replaced by Row
+-------------------------------------------------------------
 
-    >>> u1.addresses = []
+As discussed at :ref:`change_4710_core`, the Core :class:`.RowProxy` object
+is now replaced by a class called :class:`.Row`.    The base :class:`.Row`
+object now behaves more fully like a named tuple, and as such it is now
+used as the basis for tuple-like results returned by the :class:`_query.Query`
+object, rather than the previous "KeyedTuple" class.
 
-:ticket:`4519`
+The rationale is so that by SQLAlchemy 2.0, both Core and ORM SELECT statements
+will return result rows using the same :class:`.Row` object which behaves  like
+a named tuple.  Dictionary-like functionality is available from :class:`.Row`
+via the :attr:`.Row._mapping` attribute.   In the interim, Core result sets
+will make use of a :class:`.Row` subclass :class:`.LegacyRow` which maintains
+the previous dict/tuple hybrid behavior for backwards compatibility while the
+:class:`.Row` class will be used directly for ORM tuple results returned
+by the :class:`_query.Query` object.
 
-.. _change_4662:
+Effort has been made to get most of the featureset of :class:`.Row` to be
+available within the ORM, meaning that access by string name as well
+as entity / column should work::
 
-The "New instance conflicts with existing identity" error is now a warning
----------------------------------------------------------------------------
+    row = s.query(User, Address).join(User.addresses).first()
 
-SQLAlchemy has always had logic to detect when an object in the :class:`.Session`
-to be inserted has the same primary key as an object that is already present::
+    row._mapping[User]  # same as row[0]
+    row._mapping[Address]  # same as row[1]
+    row._mapping["User"]  # same as row[0]
+    row._mapping["Address"]  # same as row[1]
 
-    class Product(Base):
-        __tablename__ = 'product'
+    u1 = aliased(User)
+    row = s.query(u1).only_return_tuples(True).first()
+    row._mapping[u1]  # same as row[0]
 
-        id = Column(Integer, primary_key=True)
 
-    session = Session(engine)
+    row = (
+        s.query(User.id, Address.email_address)
+        .join(User.addresses)
+        .first()
+    )
 
-    # add Product with primary key 1
-    session.add(Product(id=1))
-    session.flush()
+    row._mapping[User.id]  # same as row[0]
+    row._mapping["id"]  # same as row[0]
+    row._mapping[users.c.id]  # same as row[0]
 
-    # add another Product with same primary key
-    session.add(Product(id=1))
-    s.commit()  # <-- will raise FlushError
+.. seealso::
 
-The change is that the :class:`.FlushError` is altered to be only a warning::
+    :ref:`change_4710_core`
 
-    sqlalchemy/orm/persistence.py:408: SAWarning: New instance <Product at 0x7f1ff65e0ba8> with identity key (<class '__main__.Product'>, (1,), None) conflicts with persistent instance <Product at 0x7f1ff60a4550>
+:ticket:`4710`.
 
+.. _change_5074:
 
-Subsequent to that, the condition will attempt to insert the row into the
-database which will emit :class:`.IntegrityError`, which is the same error that
-would be raised if the primary key identity was not already present in the
-:class:`.Session`::
+Session does not immediately create a new SessionTransaction object
+----------------------------------------------------------------------------
 
-    sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: product.id
+The :class:`.Session` object's default behavior of ``autocommit=False``
+historically has meant that there is always a :class:`.SessionTransaction`
+object in play, associated with the :class:`.Session` via the
+:attr:`.Session.transaction` attribute.   When the given
+:class:`.SessionTransaction` was complete, due to a commit, rollback, or close,
+it was immediately replaced with a new one.  The :class:`.SessionTransaction`
+by itself does not imply the usage of any connection-oriented resources, so
+this long-standing behavior has a particular elegance to it in that the state
+of :attr:`.Session.transaction` is always predictable as non-None.
 
-The rationale is to allow code that is using :class:`.IntegrityError` to catch
-duplicates to function regardless of the existing state of the
-:class:`.Session`, as is often done using savepoints::
+However, as part of the initiative in :ticket:`5056` to greatly reduce
+reference cycles, this assumption means that calling upon
+:meth:`.Session.close` results in a :class:`.Session` object that still has
+reference cycles and is more expensive to clean up, not to mention that there
+is a small overhead in constructing the :class:`.SessionTransaction`
+object, which meant that there would be unnecessary overhead created
+for a :class:`.Session` that for example invoked :meth:`.Session.commit`
+and then :meth:`.Session.close`.
 
+As such, it was decided that :meth:`.Session.close` should leave the internal
+state of ``self.transaction``, now referred to internally as
+``self._transaction``, as None, and that a new :class:`.SessionTransaction`
+should only be created when needed.  For consistency and code coverage, this
+behavior was also expanded to include all the points at which "autobegin" is
+expected, not just when :meth:`.Session.close` were called.
 
-    # add another Product with same primary key
-    try:
-        with session.begin_nested():
-            session.add(Product(id=1))
-    except exc.IntegrityError:
-        print("row already exists")
+In particular, this causes a behavioral change for applications which
+subscribe to the :meth:`.SessionEvents.after_transaction_create` event hook;
+previously, this event would be emitted when the :class:`.Session` were  first
+constructed, as well as for most actions that closed the previous transaction
+and would emit :meth:`.SessionEvents.after_transaction_end`.  The new behavior
+is that :meth:`.SessionEvents.after_transaction_create` is emitted on demand,
+when the :class:`.Session` has not yet created a  new
+:class:`.SessionTransaction` object and mapped objects are associated with the
+:class:`.Session` through methods like :meth:`.Session.add` and
+:meth:`.Session.delete`, when  the :attr:`.Session.transaction` attribute is
+called upon, when the :meth:`.Session.flush` method has tasks to complete, etc.
 
-The above logic was not fully feasible earlier, as in the case that the
-``Product`` object with the existing identity were already in the
-:class:`.Session`, the code would also have to catch :class:`.FlushError`,
-which additionally is not filtered for the specific condition of integrity
-issues.   With the change, the above block behaves consistently with the
-exception of the warning also being emitted.
+Besides the change in when the :meth:`.SessionEvents.after_transaction_create`
+event is emitted, the change should have no other user-visible impact on the
+:class:`.Session` object's behavior; the :class:`.Session` will continue to have
+the behavior that it remains usable for new operations after :meth:`.Session.close`
+is called, and the sequencing of how the :class:`.Session` interacts with the
+:class:`_engine.Engine` and the database itself should also remain unaffected, since
+these operations were already operating in an on-demand fashion.
 
-Since the logic in question deals with the primary key, all databases emit an
-integrity error in the case of primary key conflicts on INSERT.    The case
-where an error would not be raised, that would have earlier, is the extremely
-unusual scenario of a mapping that defines a primary key on the mapped
-selectable that is more restrictive than what is actually configured in the
-database schema, such as when mapping to joins of tables or when defining
-additional columns as part of a composite primary key that is not actually
-constrained in the database schema. However, these situations also work  more
-consistently in that the INSERT would theoretically proceed whether or not the
-existing identity were still in the database.  The warning can also be
-configured to raise an exception using the Python warnings filter.
+:ticket:`5074`
 
+.. _change_5237_14:
 
-:ticket:`4662`
+Viewonly relationships don't synchronize backrefs
+-------------------------------------------------
+
+In :ticket:`5149` in 1.3.14, SQLAlchemy began emitting a warning when the
+:paramref:`_orm.relationship.backref` or :paramref:`_orm.relationship.back_populates`
+keywords would be used at the same time as the :paramref:`_orm.relationship.viewonly`
+flag on the target relationship.  This was because a "viewonly" relationship does
+not actually persist changes made to it, which could cause some misleading
+behaviors to occur.  However, in :ticket:`5237`, we sought to refine this
+behavior as there are legitimate use cases to have backrefs set up on
+viewonly relationships, including that back populates attributes are used
+in some cases by the relationship lazy loaders to determine that an additional
+eager load in the other direction is not necessary, as well as that back
+populates can be used for mapper introspection and that :func:`_orm.backref`
+can be a convenient way to set up bi-directional relationships.
+
+The solution then was to make the "mutation" that occurs from a backref
+an optional thing, using the :paramref:`_orm.relationship.sync_backref`
+flag.  In 1.4 the value of :paramref:`_orm.relationship.sync_backref` defaults
+to False for a relationship target that also sets :paramref:`_orm.relationship.viewonly`.
+This indicates that any changes made to a relationship with
+viewonly will not impact the state of the other side or of the :class:`_orm.Session`
+in any way::
+
+
+    class User(Base):
+        # ...
+
+        addresses = relationship(Address, backref=backref("user", viewonly=True))
+
+    class Address(Base):
+        # ...
+
+
+    u1 = session.query(User).filter_by(name="x").first()
+
+    a1 = Address()
+    a1.user = u1
+
+Above, the ``a1`` object will **not** be added to the ``u1.addresses``
+collection, nor will the ``a1`` object be added to the session.  Previously,
+both of these things would be true.   The warning that
+:paramref:`.relationship.sync_backref` should be set to ``False`` when
+:paramref:`.relationship.viewonly` is ``False`` is no longer emitted as this is
+now the default behavior.
+
+:ticket:`5237`
 
 .. _change_5150:
 
@@ -1443,79 +1650,342 @@ use of the :paramref:`_orm.Session.future` flag to :term:`2.0-style` mode::
 
 :ticket:`5150`
 
-.. _change_4994:
+.. _change_1763:
 
-Persistence-related cascade operations disallowed with viewonly=True
----------------------------------------------------------------------
+Eager loaders emit during unexpire operations
+---------------------------------------------
 
-When a :func:`_orm.relationship` is set as ``viewonly=True`` using the
-:paramref:`_orm.relationship.viewonly` flag, it indicates this relationship should
-only be used to load data from the database, and should not be mutated
-or involved in a persistence operation.   In order to ensure this contract
-works successfully, the relationship can no longer specify
-:paramref:`_orm.relationship.cascade` settings that make no sense in terms of
-"viewonly".
+A long sought behavior was that when an expired object is accessed, configured
+eager loaders will run in order to eagerly load relationships on the expired
+object when the object is refreshed or otherwise unexpired.   This behavior has
+now been added, so that joinedloaders will add inline JOINs as usual, and
+selectin/subquery loaders will run an "immediateload" operation for a given
+relationship, when an expired object is unexpired or an object is refreshed::
 
-The primary targets here are the "delete, delete-orphan"  cascades, which
-through 1.3 continued to impact persistence even if viewonly were True, which
-is a bug; even if viewonly were True, an object would still cascade these
-two operations onto the related object if the parent were deleted or the
-object were detached.   Rather than modify the cascade operations to check
-for viewonly, the configuration of both of these together is simply
-disallowed::
+    >>> a1 = session.query(A).options(joinedload(A.bs)).first()
+    >>> a1.data = 'new data'
+    >>> session.commit()
 
-    class User(Base):
-        # ...
+Above, the ``A`` object was loaded with a ``joinedload()`` option associated
+with it in order to eagerly load the ``bs`` collection.    After the
+``session.commit()``, the state of the object is expired.  Upon accessing
+the ``.data`` column attribute, the object is refreshed and this will now
+include the joinedload operation as well::
 
-        # this is now an error
-        addresses = relationship(
-            "Address", viewonly=True, cascade="all, delete-orphan")
+    >>> a1.data
+    SELECT a.id AS a_id, a.data AS a_data, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id
+    FROM a LEFT OUTER JOIN b AS b_1 ON a.id = b_1.a_id
+    WHERE a.id = ?
 
-The above will raise::
+The behavior applies both to loader strategies applied to the
+:func:`_orm.relationship` directly, as well as with options used with
+:meth:`_query.Query.options`, provided that the object was originally loaded by that
+query.
 
-    sqlalchemy.exc.ArgumentError: Cascade settings
-    "delete, delete-orphan, merge, save-update" apply to persistence
-    operations and should not be combined with a viewonly=True relationship.
+For the "secondary" eager loaders "selectinload" and "subqueryload", the SQL
+strategy for these loaders is not necessary in order to eagerly load attributes
+on a single object; so they will instead invoke the "immediateload" strategy in
+a refresh scenario, which resembles the query emitted by "lazyload", emitted as
+an additional query::
 
-Applications that have this issue should be emitting a warning as of
-SQLAlchemy 1.3.12, and for the above error the solution is to remove
-the cascade settings for a viewonly relationship.
+    >>> a1 = session.query(A).options(selectinload(A.bs)).first()
+    >>> a1.data = 'new data'
+    >>> session.commit()
+    >>> a1.data
+    SELECT a.id AS a_id, a.data AS a_data
+    FROM a
+    WHERE a.id = ?
+    (1,)
+    SELECT b.id AS b_id, b.a_id AS b_a_id
+    FROM b
+    WHERE ? = b.a_id
+    (1,)
 
+Note that a loader option does not apply to an object that was introduced
+into the :class:`.Session` in a different way.  That is, if the ``a1`` object
+were just persisted in this :class:`.Session`, or was loaded with a different
+query before the eager option had been applied, then the object doesn't have
+an eager load option associated with it.  This is not a new concept, however
+users who are looking for the eagerload on refresh behavior may find this
+to be more noticeable.
 
-:ticket:`4993`
-:ticket:`4994`
+:ticket:`1763`
 
-.. _change_5122:
+.. _change_4519:
 
-Stricter behavior when querying inheritance mappings using custom queries
--------------------------------------------------------------------------
+Accessing an uninitialized collection attribute on a transient object no longer mutates __dict__
+-------------------------------------------------------------------------------------------------
 
-This change applies to the scenario where a joined- or single- table
-inheritance subclass entity is being queried, given a completed SELECT subquery
-to select from.   If the given subquery returns rows that do not correspond to
-the requested polymorphic identity or identities, an error is raised.
-Previously, this condition would pass silently under joined table inheritance,
-returning an invalid subclass, and under single table inheritance, the
-:class:`_query.Query` would be adding additional criteria against the subquery to
-limit the results which could inappropriately interfere with the intent of the
-query.
+It has always been SQLAlchemy's behavior that accessing mapped attributes on a
+newly created object returns an implicitly generated value, rather than raising
+``AttributeError``, such as ``None`` for scalar attributes or ``[]`` for a
+list-holding relationship::
 
-Given the example mapping of ``Employee``, ``Engineer(Employee)``, ``Manager(Employee)``,
-in the 1.3 series if we were to emit the following query against a joined
-inheritance mapping::
+    >>> u1 = User()
+    >>> u1.name
+    None
+    >>> u1.addresses
+    []
 
-    s = Session(e)
+The rationale for the above behavior was originally to make ORM objects easier
+to work with.  Since an ORM object represents an empty row when first created
+without any state, it is intuitive that its un-accessed attributes would
+resolve to ``None`` (or SQL NULL) for scalars and to empty collections for
+relationships.   In particular, it makes possible an extremely common pattern
+of being able to mutate the new collection without manually creating and
+assigning an empty collection first::
 
-    s.add_all([Engineer(), Manager()])
+    >>> u1 = User()
+    >>> u1.addresses.append(Address())  # no need to assign u1.addresses = []
 
-    s.commit()
+Up until version 1.0 of SQLAlchemy, the behavior of this initialization  system
+for both scalar attributes as well as collections would be that the ``None`` or
+empty collection would be *populated* into the object's  state, e.g.
+``__dict__``.  This meant that the following two operations were equivalent::
 
-    print(
-        s.query(Manager).select_entity_from(s.query(Employee).subquery()).all()
-    )
+    >>> u1 = User()
+    >>> u1.name = None  # explicit assignment
 
+    >>> u2 = User()
+    >>> u2.name  # implicit assignment just by accessing it
+    None
 
-The subquery selects both the ``Engineer`` and the ``Manager`` rows, and
+Where above, both ``u1`` and ``u2`` would have the value ``None`` populated
+in the value of the ``name`` attribute.  Since this is a SQL NULL, the ORM
+would skip including these values within an INSERT so that SQL-level defaults
+take place, if any, else the value defaults to NULL on the database side.
+
+In version 1.0 as part of :ref:`migration_3061`, this behavior was refined so
+that the ``None`` value was no longer populated into ``__dict__``, only
+returned.   Besides removing the mutating side effect of a getter operation,
+this change also made it possible to set columns that did have server defaults
+to the value NULL by actually assigning ``None``, which was now distinguished
+from just reading it.
+
+The change however did not accommodate for collections, where returning an
+empty collection that is not assigned meant that this mutable collection would
+be different each time and also would not be able to correctly accommodate for
+mutating operations (e.g. append, add, etc.) called upon it.    While the
+behavior continued to generally not get in anyone's way, an edge case was
+eventually identified in :ticket:`4519` where this empty collection could be
+harmful, which is when the object is merged into a session::
+
+    >>> u1 = User(id=1)  # create an empty User to merge with id=1 in the database
+    >>> merged1 = session.merge(u1)  # value of merged1.addresses is unchanged from that of the DB
+
+    >>> u2 = User(id=2) # create an empty User to merge with id=2 in the database
+    >>> u2.addresses
+    []
+    >>> merged2 = session.merge(u2)  # value of merged2.addresses has been emptied in the DB
+
+Above, the ``.addresses`` collection on ``merged1`` will contain all the
+``Address()`` objects that were already in the database.   ``merged2`` will
+not; because it has an empty list implicitly assigned, the ``.addresses``
+collection will be erased.   This is an example of where this mutating side
+effect can actually mutate the database itself.
+
+While it was considered that perhaps the attribute system should begin using
+strict "plain Python" behavior, raising ``AttributeError`` in all cases for
+non-existent attributes on non-persistent objects and requiring that  all
+collections be explicitly assigned, such a change would likely be too extreme
+for the vast number of applications that have relied upon this  behavior for
+many years, leading to a complex rollout / backwards compatibility problem as
+well as the likelihood that workarounds to restore the old behavior would
+become prevalent, thus rendering the whole change ineffective in any case.
+
+The change then is to keep the default producing behavior, but to finally make
+the non-mutating behavior of scalars a reality for collections as well, via the
+addition of additional mechanics in the collection system.  When accessing the
+empty attribute, the new collection is created and associated with the state,
+however is not added to ``__dict__`` until it is actually mutated::
+
+    >>> u1 = User()
+    >>> l1 = u1.addresses  # new list is created, associated with the state
+    >>> assert u1.addresses is l1  # you get the same list each time you access it
+    >>> assert "addresses" not in u1.__dict__  # but it won't go into __dict__ until it's mutated
+    >>> from sqlalchemy import inspect
+    >>> inspect(u1).attrs.addresses.history
+    History(added=None, unchanged=None, deleted=None)
+
+When the list is changed, then it becomes part of the tracked changes to
+be persisted to the database::
+
+    >>> l1.append(Address())
+    >>> assert "addresses" in u1.__dict__
+    >>> inspect(u1).attrs.addresses.history
+    History(added=[<__main__.Address object at 0x7f49b725eda0>], unchanged=[], deleted=[])
+
+This change is expected to have *nearly* no impact on existing applications
+in any way, except that it has been observed that some applications may be
+relying upon the implicit assignment of this collection, such as to assert that
+the object contains certain values based on its ``__dict__``::
+
+    >>> u1 = User()
+    >>> u1.addresses
+    []
+    # this will now fail, would pass before
+    >>> assert {k: v for k, v in u1.__dict__.items() if not k.startswith("_")} == {"addresses": []}
+
+or to ensure that the collection won't require a lazy load to proceed, the
+(admittedly awkward) code below will now also fail::
+
+    >>> u1 = User()
+    >>> u1.addresses
+    []
+    >>> s.add(u1)
+    >>> s.flush()
+    >>> s.close()
+    >>> u1.addresses  # <-- will fail, .addresses is not loaded and object is detached
+
+Applications that rely upon the implicit mutating behavior of collections will
+need to be changed so that they assign the desired collection explicitly::
+
+    >>> u1.addresses = []
+
+:ticket:`4519`
+
+.. _change_4662:
+
+The "New instance conflicts with existing identity" error is now a warning
+---------------------------------------------------------------------------
+
+SQLAlchemy has always had logic to detect when an object in the :class:`.Session`
+to be inserted has the same primary key as an object that is already present::
+
+    class Product(Base):
+        __tablename__ = 'product'
+
+        id = Column(Integer, primary_key=True)
+
+    session = Session(engine)
+
+    # add Product with primary key 1
+    session.add(Product(id=1))
+    session.flush()
+
+    # add another Product with same primary key
+    session.add(Product(id=1))
+    s.commit()  # <-- will raise FlushError
+
+The change is that the :class:`.FlushError` is altered to be only a warning::
+
+    sqlalchemy/orm/persistence.py:408: SAWarning: New instance <Product at 0x7f1ff65e0ba8> with identity key (<class '__main__.Product'>, (1,), None) conflicts with persistent instance <Product at 0x7f1ff60a4550>
+
+
+Subsequent to that, the condition will attempt to insert the row into the
+database which will emit :class:`.IntegrityError`, which is the same error that
+would be raised if the primary key identity was not already present in the
+:class:`.Session`::
+
+    sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: product.id
+
+The rationale is to allow code that is using :class:`.IntegrityError` to catch
+duplicates to function regardless of the existing state of the
+:class:`.Session`, as is often done using savepoints::
+
+
+    # add another Product with same primary key
+    try:
+        with session.begin_nested():
+            session.add(Product(id=1))
+    except exc.IntegrityError:
+        print("row already exists")
+
+The above logic was not fully feasible earlier, as in the case that the
+``Product`` object with the existing identity were already in the
+:class:`.Session`, the code would also have to catch :class:`.FlushError`,
+which additionally is not filtered for the specific condition of integrity
+issues.   With the change, the above block behaves consistently with the
+exception of the warning also being emitted.
+
+Since the logic in question deals with the primary key, all databases emit an
+integrity error in the case of primary key conflicts on INSERT.    The case
+where an error would not be raised, that would have earlier, is the extremely
+unusual scenario of a mapping that defines a primary key on the mapped
+selectable that is more restrictive than what is actually configured in the
+database schema, such as when mapping to joins of tables or when defining
+additional columns as part of a composite primary key that is not actually
+constrained in the database schema. However, these situations also work  more
+consistently in that the INSERT would theoretically proceed whether or not the
+existing identity were still in the database.  The warning can also be
+configured to raise an exception using the Python warnings filter.
+
+
+:ticket:`4662`
+
+.. _change_4994:
+
+Persistence-related cascade operations disallowed with viewonly=True
+---------------------------------------------------------------------
+
+When a :func:`_orm.relationship` is set as ``viewonly=True`` using the
+:paramref:`_orm.relationship.viewonly` flag, it indicates this relationship should
+only be used to load data from the database, and should not be mutated
+or involved in a persistence operation.   In order to ensure this contract
+works successfully, the relationship can no longer specify
+:paramref:`_orm.relationship.cascade` settings that make no sense in terms of
+"viewonly".
+
+The primary targets here are the "delete, delete-orphan"  cascades, which
+through 1.3 continued to impact persistence even if viewonly were True, which
+is a bug; even if viewonly were True, an object would still cascade these
+two operations onto the related object if the parent were deleted or the
+object were detached.   Rather than modify the cascade operations to check
+for viewonly, the configuration of both of these together is simply
+disallowed::
+
+    class User(Base):
+        # ...
+
+        # this is now an error
+        addresses = relationship(
+            "Address", viewonly=True, cascade="all, delete-orphan")
+
+The above will raise::
+
+    sqlalchemy.exc.ArgumentError: Cascade settings
+    "delete, delete-orphan, merge, save-update" apply to persistence
+    operations and should not be combined with a viewonly=True relationship.
+
+Applications that have this issue should be emitting a warning as of
+SQLAlchemy 1.3.12, and for the above error the solution is to remove
+the cascade settings for a viewonly relationship.
+
+
+:ticket:`4993`
+:ticket:`4994`
+
+.. _change_5122:
+
+Stricter behavior when querying inheritance mappings using custom queries
+-------------------------------------------------------------------------
+
+This change applies to the scenario where a joined- or single- table
+inheritance subclass entity is being queried, given a completed SELECT subquery
+to select from.   If the given subquery returns rows that do not correspond to
+the requested polymorphic identity or identities, an error is raised.
+Previously, this condition would pass silently under joined table inheritance,
+returning an invalid subclass, and under single table inheritance, the
+:class:`_query.Query` would be adding additional criteria against the subquery to
+limit the results which could inappropriately interfere with the intent of the
+query.
+
+Given the example mapping of ``Employee``, ``Engineer(Employee)``, ``Manager(Employee)``,
+in the 1.3 series if we were to emit the following query against a joined
+inheritance mapping::
+
+    s = Session(e)
+
+    s.add_all([Engineer(), Manager()])
+
+    s.commit()
+
+    print(
+        s.query(Manager).select_entity_from(s.query(Employee).subquery()).all()
+    )
+
+
+The subquery selects both the ``Engineer`` and the ``Manager`` rows, and
 even though the outer query is against ``Manager``, we get a non ``Manager``
 object back::
 
@@ -1592,416 +2062,6 @@ discriminator column::
 
 :ticket:`5122`
 
-
-New Features - Core
-====================
-
-.. _change_4737:
-
-
-Built-in FROM linting will warn for any potential cartesian products in a SELECT statement
-------------------------------------------------------------------------------------------
-
-As the Core expression language as well as the ORM are built on an "implicit
-FROMs" model where a particular FROM clause is automatically added if any part
-of the query refers to it, a common issue is the case where a SELECT statement,
-either a top level statement or an embedded subquery, contains FROM elements
-that are not joined to the rest of the FROM elements in the query, causing
-what's referred to as a "cartesian product" in the result set, i.e. every
-possible combination of rows from each FROM element not otherwise joined.  In
-relational databases, this is nearly always an undesirable outcome as it
-produces an enormous result set full of duplicated, uncorrelated data.
-
-SQLAlchemy, for all of its great features, is particularly prone to this sort
-of issue happening as a SELECT statement will have elements added to its FROM
-clause automatically from any table seen in the other clauses. A typical
-scenario looks like the following, where two tables are JOINed together,
-however an additional entry in the WHERE clause that perhaps inadvertently does
-not line up with these two tables will create an additional FROM entry::
-
-    address_alias = aliased(Address)
-
-    q = session.query(User).\
-        join(address_alias, User.addresses).\
-        filter(Address.email_address == 'foo')
-
-The above query selects from a JOIN of ``User`` and ``address_alias``, the
-latter of which is an alias of the ``Address`` entity.  However, the
-``Address`` entity is used within the WHERE clause directly, so the above would
-result in the SQL::
-
-    SELECT
-        users.id AS users_id, users.name AS users_name,
-        users.fullname AS users_fullname,
-        users.nickname AS users_nickname
-    FROM addresses, users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
-    WHERE addresses.email_address = :email_address_1
-
-In the above SQL, we can see what SQLAlchemy developers term "the dreaded
-comma", as we see "FROM addresses, users JOIN addresses" in the FROM clause
-which is the classic sign of a cartesian product; where a query is making use
-of JOIN in order to join FROM clauses together, however because one of them is
-not joined, it uses a comma.      The above query will return a full set of
-rows that join the "user" and "addresses" table together on the "id / user_id"
-column, and will then apply all those rows into a cartesian product against
-every row in the "addresses" table directly.   That is, if there are ten user
-rows and 100 rows in addresses, the above query will return its expected result
-rows, likely to be 100 as all address rows would be selected, multiplied by 100
-again, so that the total result size would be 10000 rows.
-
-The "table1, table2 JOIN table3" pattern is one that also occurs quite
-frequently within the SQLAlchemy ORM due to either subtle mis-application of
-ORM features particularly those related to joined eager loading or joined table
-inheritance, as well as a result of SQLAlchemy ORM bugs within those same
-systems.   Similar issues apply to SELECT statements that use "implicit joins",
-where the JOIN keyword is not used and instead each FROM element is linked with
-another one via the WHERE clause.
-
-For some years there has been a recipe on the Wiki that applies a graph
-algorithm to a :func:`_expression.select` construct at query execution time and inspects
-the structure of the query for these un-linked FROM clauses, parsing through
-the WHERE clause and all JOIN clauses to determine how FROM elements are linked
-together and ensuring that all the FROM elements are connected in a single
-graph. This recipe has now been adapted to be part of the :class:`.SQLCompiler`
-itself where it now optionally emits a warning for a statement if this
-condition is detected.   The warning is enabled using the
-:paramref:`_sa.create_engine.enable_from_linting` flag and is enabled by default.
-The computational overhead of the linter is very low, and additionally it only
-occurs during statement compilation which means for a cached SQL statement it
-only occurs once.
-
-Using this feature, our ORM query above will emit a warning::
-
-    >>> q.all()
-    SAWarning: SELECT statement has a cartesian product between FROM
-    element(s) "addresses_1", "users" and FROM element "addresses".
-    Apply join condition(s) between each element to resolve.
-
-The linter feature accommodates not just for tables linked together through the
-JOIN clauses but also through the WHERE clause  Above, we can add a WHERE
-clause to link the new ``Address`` entity with the previous ``address_alias``
-entity and that will remove the warning::
-
-    q = session.query(User).\
-        join(address_alias, User.addresses).\
-        filter(Address.email_address == 'foo').\
-        filter(Address.id == address_alias.id)  # resolve cartesian products,
-                                                # will no longer warn
-
-The cartesian product warning considers **any** kind of link between two
-FROM clauses to be a resolution, even if the end result set is still
-wasteful, as the linter is intended only to detect the common case of a
-FROM clause that is completely unexpected.  If the FROM clause is referred
-to explicitly elsewhere and linked to the other FROMs, no warning is emitted::
-
-    q = session.query(User).\
-        join(address_alias, User.addresses).\
-        filter(Address.email_address == 'foo').\
-        filter(Address.id > address_alias.id)  # will generate a lot of rows,
-                                               # but no warning
-
-Full cartesian products are also allowed if they are explicitly stated; if we
-wanted for example the cartesian product of ``User`` and ``Address``, we can
-JOIN on :func:`.true` so that every row will match with every other; the
-following query will return all rows and produce no warnings::
-
-    from sqlalchemy import true
-
-    # intentional cartesian product
-    q = session.query(User).join(Address, true())  # intentional cartesian product
-
-The warning is only generated by default when the statement is compiled by the
-:class:`_engine.Connection` for execution; calling the :meth:`_expression.ClauseElement.compile`
-method will not emit a warning unless the linting flag is supplied::
-
-    >>> from sqlalchemy.sql import FROM_LINTING
-    >>> print(q.statement.compile(linting=FROM_LINTING))
-    SAWarning: SELECT statement has a cartesian product between FROM element(s) "addresses" and FROM element "users".  Apply join condition(s) between each element to resolve.
-    SELECT users.id, users.name, users.fullname, users.nickname
-    FROM addresses, users JOIN addresses AS addresses_1 ON users.id = addresses_1.user_id
-    WHERE addresses.email_address = :email_address_1
-
-:ticket:`4737`
-
-
-
-Behavior Changes - Core
-========================
-
-.. _change_4753:
-
-SELECT objects and derived FROM clauses allow for duplicate columns and column labels
--------------------------------------------------------------------------------------
-
-This change allows that the :func:`_expression.select` construct now allows for duplicate
-column labels as well as duplicate column objects themselves, so that result
-tuples are organized and ordered in the identical way in that the columns were
-selected.  The ORM :class:`_query.Query` already works this way, so this change
-allows for greater cross-compatibility between the two, which is a key goal of
-the 2.0 transition::
-
-    >>> from sqlalchemy import column, select
-    >>> c1, c2, c3, c4 = column('c1'), column('c2'), column('c3'), column('c4')
-    >>> stmt = select([c1, c2, c3.label('c2'), c2, c4])
-    >>> print(stmt)
-    SELECT c1, c2, c3 AS c2, c2, c4
-
-To support this change, the :class:`_expression.ColumnCollection` used by
-:class:`_expression.SelectBase` as well as for derived FROM clauses such as subqueries
-also support duplicate columns; this includes the new
-:attr:`_expression.SelectBase.selected_columns` attribute, the deprecated ``SelectBase.c``
-attribute, as well as the :attr:`_expression.FromClause.c` attribute seen on constructs
-such as :class:`.Subquery` and :class:`_expression.Alias`::
-
-    >>> list(stmt.selected_columns)
-    [
-        <sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcca20; c1>,
-        <sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcc9e8; c2>,
-        <sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>,
-        <sqlalchemy.sql.elements.ColumnClause at 0x7fa540bcc9e8; c2>,
-        <sqlalchemy.sql.elements.ColumnClause at 0x7fa540897048; c4>
-    ]
-
-    >>> print(stmt.subquery().select())
-    SELECT anon_1.c1, anon_1.c2, anon_1.c2, anon_1.c2, anon_1.c4
-    FROM (SELECT c1, c2, c3 AS c2, c2, c4) AS anon_1
-
-:class:`_expression.ColumnCollection` also allows access by integer index to support
-when the string "key" is ambiguous::
-
-    >>> stmt.selected_columns[2]
-    <sqlalchemy.sql.elements.Label object at 0x7fa540b3e2e8>
-
-To suit the use of :class:`_expression.ColumnCollection` in objects such as
-:class:`_schema.Table` and :class:`.PrimaryKeyConstraint`, the old "deduplicating"
-behavior which is more critical for these objects is preserved in a new class
-:class:`.DedupeColumnCollection`.
-
-The change includes that the familiar warning ``"Column %r on table %r being
-replaced by %r, which has the same key.  Consider use_labels for select()
-statements."`` is **removed**; the :meth:`_expression.Select.apply_labels` is still
-available and is still used by the ORM for all SELECT operations, however it
-does not imply deduplication of column objects, although it does imply
-deduplication of implicitly generated labels::
-
-    >>> from sqlalchemy import table
-    >>> user = table('user', column('id'), column('name'))
-    >>> stmt = select([user.c.id, user.c.name, user.c.id]).apply_labels()
-    >>> print(stmt)
-    SELECT "user".id AS user_id, "user".name AS user_name, "user".id AS id_1
-    FROM "user"
-
-Finally, the change makes it easier to create UNION and other
-:class:`_selectable.CompoundSelect` objects, by ensuring that the number and position
-of columns in a SELECT statement mirrors what was given, in a use case such
-as::
-
-    >>> s1 = select([user, user.c.id])
-    >>> s2 = select([c1, c2, c3])
-    >>> from sqlalchemy import union
-    >>> u = union(s1, s2)
-    >>> print(u)
-    SELECT "user".id, "user".name, "user".id
-    FROM "user" UNION SELECT c1, c2, c3
-
-
-
-:ticket:`4753`
-
-
-
-.. _change_4449:
-
-Improved column labeling for simple column expressions using CAST or similar
-----------------------------------------------------------------------------
-
-A user pointed out that the PostgreSQL database has a convenient behavior when
-using functions like CAST against a named column, in that the result column name
-is named the same as the inner expression::
-
-    test=> SELECT CAST(data AS VARCHAR) FROM foo;
-
-    data
-    ------
-     5
-    (1 row)
-
-This allows one to apply CAST to table columns while not losing the column
-name (above using the name ``"data"``) in the result row.    Compare to
-databases such as MySQL/MariaDB, as well as most others, where the column
-name is taken from the full SQL expression and is not very portable::
-
-    MariaDB [test]> SELECT CAST(data AS CHAR) FROM foo;
-    +--------------------+
-    | CAST(data AS CHAR) |
-    +--------------------+
-    | 5                  |
-    +--------------------+
-    1 row in set (0.003 sec)
-
-
-In SQLAlchemy Core expressions, we never deal with a raw generated name like
-the above, as SQLAlchemy applies auto-labeling to expressions like these, which
-are up until now always a so-called "anonymous" expression::
-
-    >>> print(select([cast(foo.c.data, String)]))
-    SELECT CAST(foo.data AS VARCHAR) AS anon_1     # old behavior
-    FROM foo
-
-These anonymous expressions were necessary as SQLAlchemy's
-:class:`_engine.ResultProxy` made heavy use of result column names in order to match
-up datatypes, such as the :class:`.String` datatype which used to have
-result-row-processing behavior, to the correct column, so most importantly the
-names had to be both easy to determine in a database-agnostic manner as well as
-unique in all cases.    In SQLAlchemy 1.0 as part of :ticket:`918`, this
-reliance on named columns in result rows (specifically the
-``cursor.description`` element of the PEP-249 cursor) was scaled back to not be
-necessary for most Core SELECT constructs; in release 1.4, the system overall
-is becoming more comfortable with SELECT statements that have duplicate column
-or label names such as in :ref:`change_4753`.  So we now emulate PostgreSQL's
-reasonable behavior for simple modifications to a single column, most
-prominently with CAST::
-
-    >>> print(select([cast(foo.c.data, String)]))
-    SELECT CAST(foo.data AS VARCHAR) AS data
-    FROM foo
-
-For CAST against expressions that don't have a name, the previous logic is used
-to generate the usual "anonymous" labels::
-
-    >>> print(select([cast('hi there,' + foo.c.data, String)]))
-    SELECT CAST(:data_1 + foo.data AS VARCHAR) AS anon_1
-    FROM foo
-
-A :func:`.cast` against a :class:`.Label`, despite having to omit the label
-expression as these don't render inside of a CAST, will nonetheless make use of
-the given name::
-
-    >>> print(select([cast(('hi there,' + foo.c.data).label('hello_data'), String)]))
-    SELECT CAST(:data_1 + foo.data AS VARCHAR) AS hello_data
-    FROM foo
-
-And of course as was always the case, :class:`.Label` can be applied to the
-expression on the outside to apply an "AS <name>" label directly::
-
-    >>> print(select([cast(('hi there,' + foo.c.data), String).label('hello_data')]))
-    SELECT CAST(:data_1 + foo.data AS VARCHAR) AS hello_data
-    FROM foo
-
-
-:ticket:`4449`
-
-.. _change_4808:
-
-New "post compile" bound parameters used for LIMIT/OFFSET in Oracle, SQL Server
--------------------------------------------------------------------------------
-
-A major goal of the 1.4 series is to establish that all Core SQL constructs
-are completely cacheable, meaning that a particular :class:`.Compiled`
-structure will produce an identical SQL string regardless of any SQL parameters
-used with it, which notably includes those used to specify the LIMIT and
-OFFSET values, typically used for pagination and "top N" style results.
-
-While SQLAlchemy has used bound parameters for LIMIT/OFFSET schemes for many
-years, a few outliers remained where such parameters were not allowed, including
-a SQL Server "TOP N" statement, such as::
-
-    SELECT TOP 5 mytable.id, mytable.data FROM mytable
-
-as well as with Oracle, where the FIRST_ROWS() hint (which SQLAlchemy will
-use if the ``optimize_limits=True`` parameter is passed to
-:func:`_sa.create_engine` with an Oracle URL) does not allow them,
-but also that using bound parameters with ROWNUM comparisons has been reported
-as producing slower query plans::
-
-    SELECT anon_1.id, anon_1.data FROM (
-        SELECT /*+ FIRST_ROWS(5) */
-        anon_2.id AS id,
-        anon_2.data AS data,
-        ROWNUM AS ora_rn FROM (
-            SELECT mytable.id, mytable.data FROM mytable
-        ) anon_2
-        WHERE ROWNUM <= :param_1
-    ) anon_1 WHERE ora_rn > :param_2
-
-In order to allow for all statements to be unconditionally cacheable at the
-compilation level, a new form of bound parameter called a "post compile"
-parameter has been added, which makes use of the same mechanism as that
-of "expanding IN parameters".  This is a :func:`.bindparam` that behaves
-identically to any other bound parameter except that parameter value will
-be rendered literally into the SQL string before sending it to the DBAPI
-``cursor.execute()`` method.   The new parameter is used internally by the
-SQL Server and Oracle dialects, so that the drivers receive the literal
-rendered value but the rest of SQLAlchemy can still consider this as a
-bound parameter.   The above two statements when stringified using
-``str(statement.compile(dialect=<dialect>))`` now look like::
-
-    SELECT TOP [POSTCOMPILE_param_1] mytable.id, mytable.data FROM mytable
-
-and::
-
-    SELECT anon_1.id, anon_1.data FROM (
-        SELECT /*+ FIRST_ROWS([POSTCOMPILE__ora_frow_1]) */
-        anon_2.id AS id,
-        anon_2.data AS data,
-        ROWNUM AS ora_rn FROM (
-            SELECT mytable.id, mytable.data FROM mytable
-        ) anon_2
-        WHERE ROWNUM <= [POSTCOMPILE_param_1]
-    ) anon_1 WHERE ora_rn > [POSTCOMPILE_param_2]
-
-The ``[POSTCOMPILE_<param>]`` format is also what is seen when an
-"expanding IN" is used.
-
-When viewing the SQL logging output, the final form of the statement will
-be seen::
-
-    SELECT anon_1.id, anon_1.data FROM (
-        SELECT /*+ FIRST_ROWS(5) */
-        anon_2.id AS id,
-        anon_2.data AS data,
-        ROWNUM AS ora_rn FROM (
-            SELECT mytable.id AS id, mytable.data AS data FROM mytable
-        ) anon_2
-        WHERE ROWNUM <= 8
-    ) anon_1 WHERE ora_rn > 3
-
-
-The "post compile parameter" feature is exposed as public API through the
-:paramref:`.bindparam.literal_execute` parameter, however is currently not
-intended for general use.   The literal values are rendered using the
-:meth:`.TypeEngine.literal_processor` of the underlying datatype, which in
-SQLAlchemy has **extremely limited** scope, supporting only integers and simple
-string values.
-
-:ticket:`4808`
-
-.. _change_4712:
-
-Connection-level transactions can now be inactive based on subtransaction
--------------------------------------------------------------------------
-
-A :class:`_engine.Connection` now includes the behavior where a :class:`.Transaction`
-can be made inactive due to a rollback on an inner transaction, however the
-:class:`.Transaction` will not clear until it is itself rolled back.
-
-This is essentially a new error condition which will disallow statement
-executions to proceed on a :class:`_engine.Connection` if an inner "sub" transaction
-has been rolled back.  The behavior works very similarly to that of the
-ORM :class:`.Session`, where if an outer transaction has been begun, it needs
-to be rolled back to clear the invalid transaction; this behavior is described
-in :ref:`faq_session_rollback`
-
-While the :class:`_engine.Connection` has had a less strict behavioral pattern than
-the :class:`.Session`, this change was made as it helps to identify when
-a subtransaction has rolled back the DBAPI transaction, however the external
-code isn't aware of this and attempts to continue proceeding, which in fact
-runs operations on a new transaction.   The "test harness" pattern described
-at :ref:`session_external_transaction` is the common place for this to occur.
-
-The new behavior is described in the errors page at :ref:`error_8s2a`.
-
-
 Dialect Changes
 ===============
 
index ce26e78916edabf7a95063245d7a382a0ff878bd..d6077462e2a4bb79f6743f8f050b9ba0d8bc23b3 100644 (file)
@@ -530,7 +530,7 @@ ResultProxy replaced with Result which has more refined methods and behaviors
 
   Review the new future API for result sets:
 
-    :class:`_future.Result`
+    :class:`_engine.Result`
 
 
 A major goal of SQLAlchemy 2.0 is to unify how "results" are handled between
@@ -747,18 +747,22 @@ pattern which basically does the same thing.
 ORM Query Unified with Core Select
 ==================================
 
-.. admonition:: Certainty: tentative
-
-  Tenative overall, however there will almost definitely be
-  architectural changes in :class:`_query.Query` that move it closer to
-  :func:`_expression.select`.
+.. admonition:: Certainty: definite
 
-  The ``session.query(<cls>)`` pattern itself will likely **not** be fully
-  removed.   As this pattern is extremely prevalent and numerous within any
-  individual application, and that it does not intrinsically suggest an
-  "antipattern" from a development standpoint, at the moment we are hoping
-  that a transition to 2.0 won't require a rewrite of every ``session.query()``
-  call, however it will be a legacy pattern that may warn as such.
+    This is now implemented in 1.4.  The :class:`_orm.Query` object now
+    generates a :class:`_sql.Select` object, which is then executed
+    via :meth:`_orm.Session.execute`.  The API to instead use :class:`_sql.Select`
+    and :meth:`_orm.Session.execute` directly, foregoing the usage of
+    :class:`_orm.Query` altogether, is fully available in 1.4.   Most internal
+    ORM systems for loading and refreshing objects has been transitioned to
+    use :class:`_sql.Select` directly.
+
+    The ``session.query(<cls>)`` pattern itself will likely **not** be fully
+    removed.   As this pattern is extremely prevalent and numerous within any
+    individual application, and that it does not intrinsically suggest an
+    "antipattern" from a development standpoint, at the moment we are hoping
+    that a transition to 2.0 won't require a rewrite of every ``session.query()``
+    call, however it will be a legacy pattern that may warn as such.
 
 Ever wonder why SQLAlchemy :func:`_expression.select` uses :meth:`_expression.Select.where` to add
 a WHERE clause and :class:`_query.Query` uses :meth:`_query.Query.filter` ?   Same here!
@@ -1082,9 +1086,10 @@ The above query will disambiguate the ``.id`` column of ``User`` and
 Transparent Statement Compilation Caching replaces "Baked" queries, works in Core
 ==================================================================================
 
-.. admonition:: Certainty: tentative
+.. admonition:: Certainty: definite
 
-  Pending further architectural prototyping and performance testing
+  This is now implemented in 1.4.   The migration notes at :ref:`change_4639`
+  detail the change.
 
 A major restructuring of the Core internals as well as of that of the ORM
 :class:`_query.Query` will be reorganizing the major statement objects to have very
@@ -1136,13 +1141,15 @@ it will be fully transparent.   Applications that wish to reduce statement
 building latency even further to the levels currently offered by the "baked"
 system can opt to use the "lambda" constructs.
 
-Uniquifying ORM Rows
-====================
+ORM Rows not uniquified by default
+===================================
 
-.. admonition:: Certainty: tentative
+.. admonition:: Certainty: likely
 
-  However this is a widely requested behavior so
-  it's likely something will have to happen in this regard
+    This is now partially implemented for the :term:`2.0 style` use of ORM
+    queries, in that rows are not automatically uniquified unless unique() is
+    called. However we have yet to receive user feedback (or
+    complaints) on this change.
 
 ORM rows returned by ``session.execute(stmt)`` are no longer automatically
 "uniqued"; this must be called explicitly::
@@ -1171,10 +1178,10 @@ will now be the same.
 Tuples, Scalars, single-row results with ORM / Core results made consistent
 ============================================================================
 
-.. admonition:: Certainty: tentative
+.. admonition:: Certainty: likely
 
-    Again this is an often requested behavior
-    at the ORM level so something will have to happen in this regard
+    This is also implemented for :term:`2.0 style` ORM use however we don't
+    have user feedback yet.
 
 The :meth:`.future.Result.all` method now delivers named-tuple results
 in all cases, even for an ORM select that is against a single entity.   This
@@ -1240,73 +1247,132 @@ The same pattern is needed for "dynamic" relationships::
     user.addresses.where(Address.id > 10).execute().all()
 
 
-What about asyncio???
+Asyncio Support
 =====================
 
-.. admonition:: Certainty: tentative
-
-  Not much is really being proposed here except a willingness to continue
-  working with third-party extensions and contributors who want to work on
-  the problem, as well as hopefully making the task of integration a little
-  bit more straightforward.
-
-How can SQLAlchemy do a whole re-think for Python 3 only and not take into
-account asyncio?   The current thinking here is going to be mixed for fans
-of asyncio-everything, here are the bulletpoints:
-
-* As is likely well known SQLAlchemy developers maintain that `asyncio with
-  SQL queries usually not that compelling of an
-  idea <https://techspot.zzzeek.org/2015/02/15/asynchronous-python-and-databases/>`_
-
-* There's almost no actual advantage to having an "asyncio" version of
-  SQLAlchemy other than personal preference and arguably interoperability
-  with existing asyncio code (however thread executors remain probably a
-  better option).   Database connections do not
-  usually fit the criteria of the kind of socket connection that benefits
-  by being accessed in a non-blocking way, since they are usually local,
-  fast services that are accessed on a connection-limited scale.  This is
-  in complete contrast to the use case for non-blocking IO which is massively
-  scaled connections to sockets that are arbitrarily slow and/or sleepy.
-
-* Nevertheless, lots of Python programmers like the asyncio approach and feel
-  more comfortable working with requests in the inherently "callback"
-  style of event-based programming.  SQLAlchemy has every desire for these
-  people to be happy.
-
-* Making things complicated is that Python doesn't have a `spec for an asyncio
-  DBAPI <https://discuss.python.org/t/asynchronous-dbapi/2206/>`_ as of yet, which
-  makes it pretty tough for DBAPIs to exist without them all being dramatically
-  different in how they work and would be integrated.
-
-* There are however a few DBAPIs for PostgreSQL that are truly non-blocking,
-  as well as at least one for MySQL that works with non-blocking IO.  It's not
-  known if any such system exists for SQLite, Oracle, ODBC datasources, SQL
-  Server, etc.
-
-* There are (more than one?) extensions of SQLAlchemy right now which basically
-  pick and choose a few parts of the compilation APIs and then reimplement
-  their own engine implementation completely, such as `aiopg <https://github.com/aio-libs/aiopg/blob/master/aiopg/sa/connection.py>`_.
-
-* These implementations appear to be useful for users however they aren't able
-  to keep up with SQLAlchemy's own capabilities and they likely don't really
-  work for lots of existing use cases either.
-
-* Essentially, it is hoped that the re-architecting of :class:`_engine.Connection`
-  to no longer support things like "autocommit" and "connectionless"
-  execution, as well as the changes to how result fetching will work with the
-  ``Result`` which is hoped to be simpler in how it interacts with
-  the cursor, will make it **much easier** to build async versions of
-  SQLAlchemy's :class:`_engine.Connection`.  The simplified model of
-  ``Connection.execute()`` and ``Session.execute()`` as the single point of
-  invocation of queries should also make things easier.
-
-* SQLAlchemy has always remained `fully open
-  <https://github.com/sqlalchemy/sqlalchemy/issues/3414>`_ to having a real
-  asyncio extension present as part of SQLAlchemy itself.   However this would
-  require **dedicated, long term maintainers** in order for it to be a thing.
-
-* It's probably better that such approaches remain third party, however it
-  is hoped that architectural changes in SQLAlchemy will make such approaches
-  more straightforward to implement and track SQLAlchemy's capabilities.
+.. admonition:: Certainty: definite
 
+  A surprising development will allow asyncio support including with the
+  ORM to be fully implemented.   There will even be a **completely optional**
+  path to having lazy loading be available, for those willing to make use of
+  some "controversial" patterns.
+
+There was an entire section here detailing how asyncio is a nice to have,
+but not really necessary, there are some approaches already, and maybe
+third parties can keep doing it.
+
+What's changed is that there is now an approach to doing this in SQLAlchemy
+directly that does not impact the existing library nor does it imply an
+entirely separate version of everything be maintained.  What has *not* changed
+is that asyncio is not very necessary for relational databases but **that's
+fine, we will have asyncio, no more need to debate :) :) :)**.
+
+The proof of concept at https://gist.github.com/zzzeek/4e89ce6226826e7a8df13e1b573ad354
+illustrates how to write an asyncio application that makes use of a pure asyncio
+driver (asyncpg), with part of the code **in between** remaining as sync code
+without the use of any await/async keywords.  The central technique involves
+minimal use of a greenlet (e.g. stackless Python) to perform the necessary
+context switches when an "await" occurs.   The approach has been vetted
+both with asyncio developers as well as greenlet developers, the latter
+of which contributed a great degree of simplification the already simple recipe
+such that can context switch async coroutines with no decrease in performance.
+
+The proof of concept has then been expanded to work within SQLAlchemy Core
+and is presently in a Gerrit review.   A SQLAlchemy dialect for the asyncpg
+driver has been written and it passes most tests.
+
+Example ORM use will look similar to the following; this example is already
+runnable with the in-review codebase::
+
+    import asyncio
+
+    from sqlalchemy.asyncio import create_async_engine
+    from sqlalchemy.asyncio import AsyncSession
+    # ... other imports ...
+
+    async def async_main():
+        engine = create_async_engine(
+            "postgresql+asyncpg://scott:tiger@localhost/test", echo=True,
+        )
+
+
+        # assume a typical ORM model with classes A and B
+
+        session = AsyncSession(engine)
+        session.add_all(
+            [
+                A(bs=[B(), B()], data="a1"),
+                A(bs=[B()], data="a2"),
+                A(bs=[B(), B()], data="a3"),
+            ]
+        )
+        await session.commit()
+        stmt = select(A).options(selectinload(A.bs))
+        result = await session.execute(stmt)
+        for a1 in result.scalars():
+            print(a1)
+            for b1 in a1.bs:
+                print(b1)
+
+        result = await session.execute(select(A).order_by(A.id))
+
+        a1 = result.scalars().first()
+        a1.data = "new data"
+        await session.commit()
+
+    asyncio.run(async_main())
+
+The "controversial" feature, if provided, would include that the "greenlet"
+context would be supplied as front-facing API.  This would allow an asyncio
+application to spawn a greenlet that contains sync-code, which could use the
+Core and ORM in a fully traditional manner including that lazy loading
+for columns and relationships would be present.  This mode of use is
+somewhat similar to running an application under an event-based
+programming library such as gevent or eventlet, however the underyling
+network calls would be within a pure asyncio context, i.e. like that of the
+asyncpg driver.   An example of this use, which is also runnable with
+the in-review codebase::
+
+    import asyncio
+
+    from sqlalchemy.asyncio import greenlet_spawn
+
+    from sqlalchemy import create_engine
+    from sqlalchemy.orm import Session
+    # ... other imports ...
+
+    def main():
+        # standard "sync" engine with the "async" driver.
+        engine = create_engine(
+            "postgresql+asyncpg://scott:tiger@localhost/test", echo=True,
+        )
+
+        # assume a typical ORM model with classes A and B
+
+        session = Session(engine)
+        session.add_all(
+            [
+                A(bs=[B(), B()], data="a1"),
+                A(bs=[B()], data="a2"),
+                A(bs=[B(), B()], data="a3"),
+            ]
+        )
+        session.commit()
+        for a1 in session.query(A).all():
+            print("a: %s" % a1)
+            print("bs: %s" % (a1.bs))  # emits a lazyload.
+
+    asyncio.run(greenlet_spawn(main))
+
+
+Above, we see a ``main()`` function that contains within it a 100% normal
+looking Python program using the SQLAlchemy ORM, using plain ORM imports and
+basically absolutely nothing out of the ordinary.  It just happens to be called
+from inside of an ``asyncio.run()`` call rather than directly, and it uses a
+DBAPI that is only compatible with asyncio.   There is no "monkeypatching" or
+anything else like that involved.    Any asyncio program can opt
+to place it's database-related business methods into the above pattern,
+if preferred, rather than using the asyncio SQLAlchemy API directly.  Or not.
+The greenlet technique, which is also being ported to other frameworks
+suich as Flask, has now made it so that it **no longer matters**.
 
index 6de5058c14b1142e2227cde6ce5b72643117b66e..35fa9204a1ef20f659999293624152593ac77d38 100644 (file)
@@ -13,7 +13,6 @@
 
         :ref:`loader_option_criteria`
 
-        :func:`_orm.with_loader_criteria`
+        :ref:`do_orm_execute_global_criteria`
 
-    .. TODO: add links to new examples section and session-related
-       documentation involving do_orm_execute event when merged
\ No newline at end of file
+        :func:`_orm.with_loader_criteria`
index 710ab58fe6c97a80355bd76d366ab22620a33d2d..13d573296077061a073eaf35de86b3ad73b2616b 100644 (file)
@@ -37,7 +37,7 @@ extensions = [
     "changelog",
     "sphinx_paramlinks",
 ]
-needs_extensions = {"zzzeeksphinx": "1.1.5"}
+needs_extensions = {"zzzeeksphinx": "1.1.6"}
 
 # Add any paths that contain templates here, relative to this directory.
 # not sure why abspath() is needed here, some users
@@ -130,6 +130,7 @@ zzzeeksphinx_module_prefixes = {
     "_types": "sqlalchemy.types",
     "_expression": "sqlalchemy.sql.expression",
     "_sql": "sqlalchemy.sql.expression",
+    "_dml": "sqlalchemy.sql.expression",
     "_functions": "sqlalchemy.sql.functions",
     "_pool": "sqlalchemy.pool",
     "_event": "sqlalchemy.event",
@@ -137,7 +138,7 @@ zzzeeksphinx_module_prefixes = {
     "_exc": "sqlalchemy.exc",
     "_reflection": "sqlalchemy.engine.reflection",
     "_orm": "sqlalchemy.orm",
-    "_query": "sqlalchemy.orm.query",
+    "_query": "sqlalchemy.orm",
     "_ormevent": "sqlalchemy.orm.event",
     "_ormexc": "sqlalchemy.orm.exc",
     "_baked": "sqlalchemy.ext.baked",
index aa41a868a1639318a15690156eeec1e14ddc9602..0e0170d435460e53e0e9f46b1a63f79d7244897c 100644 (file)
@@ -560,8 +560,8 @@ value as it uses bound parameters.  Subsequent invocations of the above
 within the scope of the ``connection.execute()`` call for enhanced performance.
 
 .. note:: it is important to note that the SQL compilation cache is caching
-   the **SQL string that is passed to the database only**, and **not** the
-   results returned by a query.   It is in no way a data cache and does not
+   the **SQL string that is passed to the database only**, and **not the data**
+   returned by a query.   It is in no way a data cache and does not
    impact the results returned for a particular SQL statement nor does it
    imply any memory use linked to fetching of result rows.
 
@@ -866,7 +866,7 @@ The cache can also be disabled with this argument by sending a value of
 Using Lambdas to add significant speed gains to statement production
 --------------------------------------------------------------------
 
-.. warning:: This technique is generally non-essential except in very performance
+.. deepalchemy:: This technique is generally non-essential except in very performance
    intensive scenarios, and intended for experienced Python programmers.
    While fairly straightforward, it involves metaprogramming concepts that are
    not appropriate for novice Python developers.  The lambda approach can be
index fddc535ed64a942abc06501793b0f8e462e7eaf7..82e81aad980d1ea26a74446f3529ae47afcd5c64 100644 (file)
@@ -16,7 +16,7 @@ Glossary
         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)
+        throughout the 1.x series of SQLAlchemy 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".
@@ -25,6 +25,52 @@ Glossary
 
             :ref:`migration_20_toplevel`
 
+        **Enabling 2.0 style usage**
+
+        When using code from a documentation example that indicates
+        :term:`2.0-style`, the :class:`_engine.Engine` as well as the
+        :class:`_orm.Session` in use should make use of "future" mode,
+        via the :paramref:`_sa.create_engine.future` and
+        :paramref:`_orm.Session.future` flags::
+
+            from sqlalchemy import create_engine
+            from sqlalchemy.orm import sessionmaker
+
+
+            engine = create_engine("mysql://user:pass:host/dbname", future=True)
+            Session = sessionmaker(bind=engine, future=True)
+
+        **ORM Queries in 2.0 style**
+
+        Besides the above changes to :class:`_engine.Engine` and
+        :class:`_orm.Session`, probably the most major API change implied by
+        1.x->2.0 is the migration from using the :class:`_orm.Query` object for
+        ORM SELECT statements and instead using the :func:`_sql.select`
+        construct in conjunction with the :meth:`_orm.Session.execute` method.
+        The general change looks like the following.  Given a
+        :class:`_orm.Session` and a :class:`_orm.Query` against that
+        :class:`_orm.Session`::
+
+            list_of_users = session.query(User).join(User.addresses).all()
+
+        The new style constructs the query separately from the
+        :class:`_orm.Session` using the :func:`_sql.select` construct; when
+        populated with ORM entities like the ``User`` class from the :ref:`ORM
+        Tutorial <ormtutorial_toplevel>`, the resulting :class:`_sql.Select`
+        construct receives additional "plugin" state that allows it to work
+        like the :class:`_orm.Query`::
+
+
+            from sqlalchemy import select
+
+            stmt = select(User).join(User.addresses)
+
+            session = Session()  # make sure future=True is used for 1.4
+
+            result = session.execute(stmt)
+
+            list_of_users = result.scalars().all()
+
     relational
     relational algebra
 
index cb7b8aa6d961972544bd58434fb42b404e87056d..6afef508336effbb838386226e768b144baf610c 100644 (file)
@@ -44,8 +44,7 @@ of Python objects, proceed first to the tutorial.
 
 * **ORM Usage:**
   :doc:`Session Usage and Guidelines <orm/session>` |
-  :doc:`Loading Objects <orm/loading_objects>` |
-  :doc:`Cached Query Extension <orm/extensions/baked>`
+  :doc:`Loading Objects <orm/loading_objects>`
 
 * **Extending the ORM:**
   :doc:`ORM Events and Internals <orm/extending>`
index ecf0cc65bdf90602019879893d03c43109b196f1..1db1137e0850f4d77017204647efa05d2ecb879d 100644 (file)
@@ -10,36 +10,101 @@ For an introduction to the most commonly used ORM events, see the section
 at :ref:`event_toplevel`.  Non-ORM events such as those regarding connections
 and low-level statement execution are described in :ref:`core_event_toplevel`.
 
-.. _orm_attribute_events:
+Session Events
+--------------
 
-Attribute Events
-----------------
+The most basic event hooks are available at the level of the ORM
+:class:`_orm.Session` object.   The types of things that are intercepted
+here include:
+
+* **Persistence Operations** - the ORM flush process that sends changes to the
+  database can be extended using events that fire off at different parts of the
+  flush, to augment or modify the data being sent to the database or to allow
+  other things to happen when persistence occurs.   Read more about persistence
+  events at :ref:`session_persistence_events`.
 
-.. autoclass:: sqlalchemy.orm.events.AttributeEvents
+* **Object lifecycle events** - hooks when objects are added, persisted,
+  deleted from sessions.   Read more about these at
+  :ref:`session_lifecycle_events`.
+
+* **Execution Events** - Part of the :term:`2.0 style` execution model, all
+  SELECT statements against ORM entities emitted, as well as bulk UPDATE
+  and DELETE statements outside of the flush process, are intercepted
+  from the :meth:`_orm.Session.execute` method using the
+  :meth:`_orm.SessionEvents.do_orm_execute` method.  Read more about this
+  event at :ref:`session_execute_events`.
+
+Be sure to read the :ref:`session_events_toplevel` chapter for context
+on these events.
+
+.. autoclass:: sqlalchemy.orm.SessionEvents
    :members:
 
 Mapper Events
 -------------
 
-.. autoclass:: sqlalchemy.orm.events.MapperEvents
+Mapper event hooks encompass things that happen as related to individual
+or multiple :class:`_orm.Mapper` objects, which are the central configurational
+object that maps a user-defined class to a :class:`_schema.Table` object.
+Types of things which occur at the :class:`_orm.Mapper` level include:
+
+* **Per-object persistence operations** - the most popular mapper hooks are the
+  unit-of-work hooks such as :meth:`_orm.MapperEvents.before_insert`,
+  :meth:`_orm.MapperEvents.after_update`, etc.  These events are contrasted to
+  the more coarse grained session-level events such as
+  :meth:`_orm.SessionEvents.before_flush` in that they occur within the flush
+  process on a per-object basis; while finer grained activity on an object is
+  more straightforward, availability of :class:`_orm.Session` features is
+  limited.
+
+* **Mapper configuration events** - the other major class of mapper hooks are
+  those which occur as a class is mapped, as a mapper is finalized, and when
+  sets of mappers are configured to refer to each other.  These events include
+  :meth:`_orm.MapperEvents.instrument_class`,
+  :meth:`_orm.MapperEvents.before_mapper_configured` and
+  :meth:`_orm.MapperEvents.mapper_configured` at the individual
+  :class:`_orm.Mapper` level, and  :meth:`_orm.MapperEvents.before_configured`
+  and :meth:`_orm.MapperEvents.after_configured` at the level of collections of
+  :class:`_orm.Mapper` objects.
+
+.. autoclass:: sqlalchemy.orm.MapperEvents
    :members:
 
 Instance Events
 ---------------
 
-.. autoclass:: sqlalchemy.orm.events.InstanceEvents
+Instance events are focused on the construction of ORM mapped instances,
+including when they are instantiated as :term:`transient` objects,
+when they are loaded from the database and become :term:`persistent` objects,
+as well as when database refresh or expiration operations occur on the object.
+
+.. autoclass:: sqlalchemy.orm.InstanceEvents
    :members:
 
-Session Events
---------------
 
-.. autoclass:: sqlalchemy.orm.events.SessionEvents
+
+.. _orm_attribute_events:
+
+Attribute Events
+----------------
+
+Attribute events are triggered as things occur on individual attributes of
+ORM mapped objects.  These events form the basis for things like
+:ref:`custom validation functions <simple_validators>` as well as
+:ref:`backref handlers <relationships_backref>`.
+
+.. seealso::
+
+  :ref:`mapping_attributes_toplevel`
+
+.. autoclass:: sqlalchemy.orm.AttributeEvents
    :members:
 
+
 Query Events
 ------------
 
-.. autoclass:: sqlalchemy.orm.events.QueryEvents
+.. autoclass:: sqlalchemy.orm.QueryEvents
    :members:
 
 Instrumentation Events
@@ -47,6 +112,6 @@ Instrumentation Events
 
 .. automodule:: sqlalchemy.orm.instrumentation
 
-.. autoclass:: sqlalchemy.orm.events.InstrumentationEvents
+.. autoclass:: sqlalchemy.orm.InstrumentationEvents
    :members:
 
index e8bb894fd4ff38d4ca037d047290e5f8be4ca1b4..7a79104b9bedd50e1252d7b0d86e350ded6da2ab 100644 (file)
@@ -147,6 +147,13 @@ Horizontal Sharding
 Extending the ORM
 =================
 
+.. _examples_session_orm_events:
+
+ORM Query Events
+-----------------
+
+.. automodule:: examples.extending_query
+
 .. _examples_caching:
 
 Dogpile Caching
index 31e543a85ac33d21a63bbc52d6a00c18e152aa43..04800ffc00f2ce130820b948e8dac94efff14eed 100644 (file)
@@ -2,6 +2,11 @@
 Events and Internals
 ====================
 
+The SQLAlchemy ORM as well as Core are extended generally through the use
+of event hooks.  Be sure to review the use of the event system in general
+at :ref:`event_toplevel`.
+
+
 .. toctree::
     :maxdepth: 2
 
index e8651dbaaa0901064f4bbc8dd44dc3148bc5cc9b..4751fef3638c02d5c4b0791f8d2c3ed77b9f32bb 100644 (file)
@@ -26,7 +26,7 @@ the caching of the SQL calls and result sets themselves is available in
    action taken by the user, using the system described at :ref:`sql_caching`.
 
 
-.. note::
+.. deepalchemy::
 
     The :mod:`sqlalchemy.ext.baked` extension is **not for beginners**.  Using
     it correctly requires a good high level understanding of how SQLAlchemy, the
index 08434d3bbe25910fab6003ea619895eea28fb0f1..1a06b73b85973072fa759bf7e5b028f9936411fc 100644 (file)
@@ -85,6 +85,10 @@ sections, are listed here.
 
 .. autodata:: sqlalchemy.orm.interfaces.NOT_EXTENSION
 
+.. autofunction:: sqlalchemy.orm.loading.merge_result
+
+.. autofunction:: sqlalchemy.orm.loading.merge_frozen_result
+
 
 .. autodata:: sqlalchemy.orm.interfaces.ONETOMANY
 
index b8a0f89c948d9f67df5c3c07f3aa84c6bb596eac..a8711d2e6439b8129f055dd0675ca765fb42763d 100644 (file)
@@ -1,3 +1,5 @@
+.. _mapping_attributes_toplevel:
+
 .. currentmodule:: sqlalchemy.orm
 
 Changing Attribute Behavior
index ed45a65e7870441952009aae56a50ddc6d5dd323..592004e86d6690f124d17dfdcb81b9cf234e9c17 100644 (file)
@@ -18,16 +18,16 @@ The Query Object
 
 Following is the full interface for the :class:`_query.Query` object.
 
-.. autoclass:: sqlalchemy.orm.query.Query
+.. autoclass:: sqlalchemy.orm.Query
    :members:
 
-   .. automethod:: sqlalchemy.orm.query.Query.prefix_with
+   .. automethod:: sqlalchemy.orm.Query.prefix_with
 
-   .. automethod:: sqlalchemy.orm.query.Query.suffix_with
+   .. automethod:: sqlalchemy.orm.Query.suffix_with
 
-   .. automethod:: sqlalchemy.orm.query.Query.with_hint
+   .. automethod:: sqlalchemy.orm.Query.with_hint
 
-   .. automethod:: sqlalchemy.orm.query.Query.with_statement_hint
+   .. automethod:: sqlalchemy.orm.Query.with_statement_hint
 
 ORM-Specific Query Constructs
 =============================
index bad816967135442ad907983ccc2e9c904886d39f..ada035e957f02757db9fb08df971f70e526a90ba 100644 (file)
@@ -35,6 +35,21 @@ Session and sessionmaker()
         arguments that will assist in determining amongst a set of database
         connections which one should be used to invoke this statement.
 
+    .. attribute:: local_execution_options
+
+        Dictionary view of the execution options passed to the
+        :meth:`.Session.execute` method.  This does not include options
+        that may be associated with the statement being invoked.
+
+        .. seealso::
+
+            :attr:`_orm.ORMExecuteState.execution_options`
+
+    .. attribute::  execution_options
+        The complete dictionary of current execution options.
+
+        This is a merge of the statement level options with the
+        locally passed execution options.
 
 .. autoclass:: Session
    :members:
index afa9ae23d9e209053b26f08630ee39448bde0986..f63b7abd084d75eae1e9046c1664ebd32d46b7dc 100644 (file)
@@ -41,7 +41,6 @@ another :class:`.Session` when you want to work with them again, so that they
 can resume their normal task of representing database state.
 
 
-
 Basics of Using a Session
 =========================
 
@@ -49,8 +48,8 @@ The most basic :class:`.Session` use patterns are presented here.
 
 .. _session_getting:
 
-Instantiating
--------------
+Opening and Closing a Session
+-----------------------------
 
 The :class:`_orm.Session` may be constructed on its own or by using the
 :class:`_orm.sessionmaker` class.    It typically is passed a single
@@ -119,6 +118,8 @@ can be used by any number of functions and threads simultaenously.
 
     :class:`_orm.Session`
 
+.. _session_querying_1x:
+
 Querying (1.x Style)
 --------------------
 
@@ -159,6 +160,8 @@ The :class:`_query.Query` object is introduced in great detail in
 
     :ref:`query_api_toplevel`
 
+.. _session_querying_20:
+
 Querying (2.0 style)
 --------------------
 
@@ -166,7 +169,7 @@ Querying (2.0 style)
 
 SQLAlchemy 2.0 will standardize the production of SELECT statements across both
 Core and ORM by making direct use of the :class:`_sql.Select` object within the
-ORM, removing the need for there to be a separate :class:`_orm.query.Query`
+ORM, removing the need for there to be a separate :class:`_orm.Query`
 object.    This mode of operation is available in SQLAlchemy 1.4 right now to
 support applications that will be migrating to 2.0.   The :class:`_orm.Session`
 must be instantiated with the
index 066fe7c24ae79c9e84fdd12e086cb608c2d82b46..0040f6f47fda172705d20c22ddabf4aea50694ad 100644 (file)
@@ -1,7 +1,7 @@
 .. _session_events_toplevel:
 
-Tracking Object and Session Changes with Events
-===============================================
+Tracking queries, object and Session Changes with Events
+=========================================================
 
 SQLAlchemy features an extensive :ref:`Event Listening <event_toplevel>`
 system used throughout the Core and ORM.   Within the ORM, there are a
@@ -12,6 +12,236 @@ as some older events that aren't as relevant as they once were.  This
 section will attempt to introduce the major event hooks and when they
 might be used.
 
+.. _session_execute_events:
+
+Execute Events
+---------------
+
+.. versionadded:: 1.4  The :class:`_orm.Session` now features a single
+   comprehensive hook designed to intercept all SELECT statements made
+   on behalf of the ORM as well as bulk UPDATE and DELETE statements.
+   This hook supersedes the previous :meth:`_orm.QueryEvents.before_compile`
+   event as well :meth:`_orm.QueryEvents.before_compile_update` and
+   :meth:`_orm.QueryEvents.before_compile_delete`.
+
+:class:`_orm.Session` features a comprehensive system by which all queries
+invoked via the :meth:`_orm.Session.execute` method, which includes all
+SELECT statements emitted by :class:`_orm.Query` as well as all SELECT
+statements emitted on behalf of column and relationship loaders, may
+be intercepted and modified.   The system makes use of the
+:meth:`_orm.SessionEvents.do_orm_execute` event hook as well as the
+:class:`_orm.ORMExecuteState` object to represent the event state.
+
+
+Basic Query Interception
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:meth:`_orm.SessionEvents.do_orm_execute` is firstly useful for any kind of
+interception of a query, which includes those emitted by
+:class:`_orm.Query` with :term:`1.x style` as well as when an ORM-enabled
+:term:`2.0 style` :func:`_sql.select`,
+:func:`_sql.update` or :func:`_sql.delete` construct is delivered to
+:meth:`_orm.Session.execute`.   The :class:`_orm.ORMExecuteState` construct
+provides accessors to allow modifications to statements, parameters, and
+options::
+
+    Session = sessionmaker(engine, future=True)
+
+    @event.listens_for(Session, "do_orm_execute")
+    def _do_orm_execute(orm_execute_state):
+        if orm_execute_state.is_select:
+            # add populate_existing for all SELECT statements
+
+            orm_execute_state.update_execution_options(populate_existing=True)
+
+            # check if the SELECT is against a certain entity and add an
+            # ORDER BY if so
+            col_descriptions = orm_execute_state.statement.column_descriptions
+
+            if col_descriptions[0]['entity'] is MyEntity:
+                orm_execute_state.statement = statement.order_by(MyEntity.name)
+
+The above example illustrates some simple modifications to SELECT statements.
+At this level, the :meth:`_orm.SessionEvents.do_orm_execute` event hook intends
+to replace the previous use of the :meth:`_orm.QueryEvents.before_compile` event,
+which was not fired off consistently for various kinds of loaders; additionally,
+the :meth:`_orm.QueryEvents.before_compile` only applies to :term:`1.x style`
+use with :class:`_orm.Query` and not with :term:`2.0 style` use of
+:meth:`_orm.Session.execute`.
+
+
+.. _do_orm_execute_global_criteria:
+
+Adding global WHERE / ON criteria
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+One of the most requested query-extension features is the ability to add WHERE
+criteria to all occurrences of an entity in all queries.   This is achievable
+by making use of the :func:`_orm.with_loader_criteria` query option, which
+may be used on its own, or is ideally suited to be used within the
+:meth:`_orm.SessionEvents.do_orm_execute` event::
+
+    from sqlalchemy.orm import with_loader_criteria
+
+    Session = sessionmaker(engine, future=True)
+
+    @event.listens_for(Session, "do_orm_execute")
+    def _do_orm_execute(orm_execute_state):
+        if orm_execute_state.is_select:
+            orm_execute_state.statement = orm_execute_state.statement.options(
+                with_loader_criteria(MyEntity.public == True)
+            )
+
+Above, an option is added to all SELECT statements that will limit all queries
+against ``MyEntity`` to filter on ``public == True``.   The criteria
+will be applied to **all** loads of that class within the scope of the
+immediate query as well as subsequent relationship loads, which includes
+lazy loads, selectinloads, etc.
+
+For a series of classes that all feature some common column structure,
+if the classes are composed using a :ref:`declarative mixin <declarative_mixins>`,
+the mixin class itself may be used in conjunction with the :func:`_orm.with_loader_criteria`
+option by making use of a Python lambda.  The Python lambda will be invoked at
+query compilation time against the specific entities which match the criteria.
+Given a series of classes based on a mixin called ``HasTimestamp``::
+
+    import datetime
+
+    class HasTimestamp(object):
+        timestamp = Column(DateTime, default=datetime.datetime.now)
+
+
+    class SomeEntity(HasTimestamp, Base):
+        __tablename__ = "some_entity"
+        id = Column(Integer, primary_key=True)
+
+    class SomeOtherEntity(HasTimestamp, Base):
+        __tablename__ = "some_entity"
+        id = Column(Integer, primary_key=True)
+
+
+The above classes ``SomeEntity`` and ``SomeOtherEntity`` will each have a column
+``timestamp`` that defaults to the current date and time.   An event may be used
+to intercept all objects that extend from ``HasTimestamp`` and filter their
+``timestamp`` column on a date that is no older than one month ago::
+
+    @event.listens_for(Session, "do_orm_execute")
+    def _do_orm_execute(orm_execute_state):
+        if orm_execute_state.is_select:
+            one_month_ago = datetime.datetime.today() - datetime.timedelta(months=1)
+
+            orm_execute_state.statement = orm_execute_state.statement.options(
+                with_loader_criteria(
+                    HasTimestamp,
+                    lambda cls: cls.timestamp >= one_month_ago,
+                    include_aliases=True
+                )
+            )
+
+.. seealso::
+
+    :ref:`examples_session_orm_events` - includes working examples of the
+    above :func:`_orm.with_loader_criteria` recipes.
+
+.. _do_orm_execute_re_executing:
+
+Re-Executing Statements
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. deepalchemy:: the statement re-execution feature involves a slightly
+   intricate recursive sequence, and is intended to solve the fairly hard
+   problem of being able to re-route the execution of a SQL statement into
+   various non-SQL contexts.    The twin examples of "dogpile caching" and
+   "horizontal sharding", linked below, should be used as a guide for when this
+   rather advanced feature is appropriate to be used.
+
+The :class:`_orm.ORMExecuteState` is capable of controlling the execution of
+the given statement; this includes the ability to either not invoke the
+statement at all, allowing a pre-constructed result set retrieved from a cache to
+be returned instead, as well as the ability to invoke the same statement
+repeatedly with different state, such as invoking it against multiple database
+connections and then merging the results together in memory.   Both of these
+advanced patterns are demonstrated in SQLAlchemy's example suite as detailed
+below.
+
+When inside the :meth:`_orm.SessionEvents.do_orm_execute` event hook, the
+:meth:`_orm.ORMExecuteState.invoke_statement` method may be used to invoke
+the statement using a new nested invocation of :meth:`_orm.Session.execute`,
+which will then preempt the subsequent handling of the current execution
+in progress and instead return the :class:`_engine.Result` returned by the
+inner execution.   The event handlers thus far invoked for the
+:meth:`_orm.SessionEvents.do_orm_execute` hook within this process will
+be skipped within this nested call as well.
+
+The :meth:`_orm.ORMExecuteState.invoke_statement` method returns a
+:class:`_engine.Result` object; this object then features the ability for it to
+be "frozen" into a cacheable format and "unfrozen" into a new
+:class:`_engine.Result` object, as well as for its data to be merged with
+that of other :class:`_engine.Result` objects.
+
+E.g., using :meth:`_orm.SessionEvents.do_orm_execute` to implement a cache::
+
+    from sqlalchemy.orm import loading
+
+    cache = {}
+
+    @event.listens_for(Session, "do_orm_execute")
+    def _do_orm_execute(orm_execute_state):
+        if "my_cache_key" in orm_execute_state.execution_options:
+            cache_key = orm_execute_state.execution_options["my_cache_key"]
+
+            if cache_key in cache:
+                frozen_result = cache[cache_key]
+            else:
+                frozen_result = orm_execute_state.invoke_statement().freeze()
+                cache[cache_key] = frozen_result
+
+            return loading.merge_frozen_result(
+                orm_execute_state.session,
+                orm_execute_state.statement,
+                frozen_result,
+                load=False,
+            )
+
+With the above hook in place, an example of using the cache would look like::
+
+    stmt = select(User).where(User.name == 'sandy').execution_options(my_cache_key="key_sandy")
+
+    result = session.execute(stmt)
+
+Above, a custom execution option is passed to
+:meth:`_sql.Select.execution_options` in order to establish a "cache key" that
+will then be intercepted by the :meth:`_orm.SessionEvents.do_orm_execute` hook.  This
+cache key is then matched to a :class:`_engine.FrozenResult` object that may be
+present in the cache, and if present, the object is re-used.  The recipe makes
+use of the :meth:`_engine.Result.freeze` method to "freeze" a
+:class:`_engine.Result` object, which above will contain ORM results, such that
+it can be stored in a cache and used multiple times. In order to return a live
+result from the "frozen" result, the :func:`_orm.loading.merge_frozen_result`
+function is used to merge the "frozen" data from the result object into the
+current session.
+
+The above example is implemented as a complete example in :ref:`examples_caching`.
+
+The :meth:`_orm.ORMExecuteState.invoke_statement` method may also be called
+multiple times, passing along different information to the
+:paramref:`_orm.ORMExecuteState.invoke_statement.bind_arguments` parameter such
+that the :class:`_orm.Session` will make use of different
+:class:`_engine.Engine` objects each time.  This will return a different
+:class:`_engine.Result` object each time; these results can be merged together
+using the :meth:`_engine.Result.merge` method.  This is the technique employed
+by the :ref:`horizontal_sharding_toplevel` extension; see the source code to
+familiarize.
+
+.. seealso::
+
+    :ref:`examples_caching`
+
+    :ref:`examples_sharding`
+
+
+
+
 .. _session_persistence_events:
 
 Persistence Events
index c5f47697bfd5b93011a20ae01053b579b320aced..6bef0cee60f98b1f92fdf41de11b8f8d6995d03f 100644 (file)
@@ -654,9 +654,9 @@ 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.
+   for both :term:`2.0 style` and :term:`1.x style`engines and sessions.
+   The recipe here from previous versions such as 1.3 will also continue to
+   work for 1.x engines and sessions.
 
 
 The recipe works by establishing a :class:`_engine.Connection` within a
index a6ef57bed1129ef45d054ef3be3c25c878dcc18f..dbad10b6f85c956e113bb9f29ab5060385657150 100644 (file)
@@ -1375,9 +1375,10 @@ and ``Address`` because there's only one foreign key between them. If there
 were no foreign keys, or several, :meth:`_query.Query.join`
 works better when one of the following forms are used::
 
-    query.join(Address, User.id==Address.user_id)    # explicit condition
-    query.join(User.addresses)                       # specify relationship from left to right
-    query.join(Address, User.addresses)              # same, with explicit target
+    query.join(Address, User.id==Address.user_id)          # explicit condition
+    query.join(User.addresses)                             # specify relationship from left to right
+    query.join(Address, User.addresses)                    # same, with explicit target
+    query.join(User.addresses.and_(Address.name != 'foo')) # use relationship + additional ON criteria
 
 As you would expect, the same idea is used for "outer" joins, using the
 :meth:`_query.Query.outerjoin` function::
index de4a339a7ba363b09e176bbe9d08d3c4114ee8af..e5ea52ea092a94aba553abb4c7459fb58a112adc 100644 (file)
@@ -1,31 +1,37 @@
 """
 Illustrates how to embed
-`dogpile.cache <https://dogpilecache.readthedocs.io/>`_
-functionality within the :class:`.Query` object, allowing full cache control
+`dogpile.cache <https://dogpilecache.sqlalchemy.org/>`_
+functionality with ORM queries, allowing full cache control
 as well as the ability to pull "lazy loaded" attributes from long term cache.
 
 In this demo, the following techniques are illustrated:
 
-* Using custom subclasses of :class:`.Query`
-* Basic technique of circumventing Query to pull from a
+* Using the :meth:`_orm.SessionEvents.do_orm_execute` event hook
+* Basic technique of circumventing :meth:`_orm.Session.execute` to pull from a
   custom cache source instead of the database.
 * Rudimental caching with dogpile.cache, using "regions" which allow
   global control over a fixed set of configurations.
-* Using custom :class:`.MapperOption` objects to configure options on
-  a Query, including the ability to invoke the options
-  deep within an object graph when lazy loads occur.
+* Using custom :class:`.UserDefinedOption` objects to configure options in
+  a statement object.
+
+.. seealso::
+
+    :ref:`do_orm_execute_re_executing` - includes a general example of the
+    technique presented here.
 
 E.g.::
 
     # query for Person objects, specifying cache
-    q = Session.query(Person).options(FromCache("default"))
+    stmt = select(Person).options(FromCache("default"))
 
     # specify that each Person's "addresses" collection comes from
     # cache too
-    q = q.options(RelationshipCache(Person.addresses, "default"))
+    stmt = stmt.options(RelationshipCache(Person.addresses, "default"))
+
+    # execute and results
+    result = session.execute(stmt)
 
-    # query
-    print(q.all())
+    print(result.scalars.all())
 
 To run, both SQLAlchemy and dogpile.cache must be
 installed or on the current PYTHONPATH. The demo will create a local
diff --git a/examples/extending_query/__init__.py b/examples/extending_query/__init__.py
new file mode 100644 (file)
index 0000000..b939c26
--- /dev/null
@@ -0,0 +1,17 @@
+"""
+Recipes which illustrate augmentation of ORM SELECT behavior as used by
+:meth:`_orm.Session.execute` with :term:`2.0 style` use of
+:func:`_sql.select`, as well as the :term:`1.x style` :class:`_orm.Query`
+object.
+
+Examples include demonstrations of the :func:`_orm.with_loader_criteria`
+option as well as the :meth:`_orm.SessionEvents.do_orm_execute` hook.
+
+As of SQLAlchemy 1.4, the :class:`_orm.Query` construct is unified
+with the :class:`_expression.Select` construct, so that these two objects
+are mostly the same.
+
+
+.. autosource::
+
+"""  # noqa
diff --git a/examples/extending_query/filter_public.py b/examples/extending_query/filter_public.py
new file mode 100644 (file)
index 0000000..b6d4ec0
--- /dev/null
@@ -0,0 +1,200 @@
+"""Illustrates a global criteria applied to entities of a particular type.
+
+The example here is the "public" flag, a simple boolean that indicates
+the rows are part of a publicly viewable subcategory.  Rows that do not
+include this flag are not shown unless a special option is passed to the
+query.
+
+Uses for this kind of recipe include tables that have "soft deleted" rows
+marked as "deleted" that should be skipped, rows that have access control rules
+that should be applied on a per-request basis, etc.
+
+
+"""
+
+from sqlalchemy import Boolean
+from sqlalchemy import Column
+from sqlalchemy import event
+from sqlalchemy import orm
+from sqlalchemy import true
+from sqlalchemy.orm import Session
+
+
+@event.listens_for(Session, "do_orm_execute")
+def _add_filtering_criteria(execute_state):
+    """Intercept all ORM queries.   Add a with_loader_criteria option to all
+    of them.
+
+    This option applies to SELECT queries and adds a global WHERE criteria
+    (or as appropriate ON CLAUSE criteria for join targets)
+    to all objects of a certain class or superclass.
+
+    """
+
+    # the with_loader_criteria automatically applies itself to
+    # relationship loads as well including lazy loads.   So if this is
+    # a relationship load, assume the option was set up from the top level
+    # query.
+
+    if (
+        not execute_state.is_relationship_load
+        and not execute_state.execution_options.get("include_private", False)
+    ):
+        execute_state.statement = execute_state.statement.options(
+            orm.with_loader_criteria(
+                HasPrivate,
+                lambda cls: cls.public == true(),
+                include_aliases=True,
+            )
+        )
+
+
+class HasPrivate(object):
+    """Mixin that identifies a class as having private entities"""
+
+    public = Column(Boolean, nullable=False)
+
+
+if __name__ == "__main__":
+
+    from sqlalchemy import Integer, Column, String, ForeignKey, Boolean
+    from sqlalchemy import select
+    from sqlalchemy import create_engine
+    from sqlalchemy.orm import relationship, sessionmaker
+    from sqlalchemy.ext.declarative import declarative_base
+
+    Base = declarative_base()
+
+    class User(HasPrivate, Base):
+        __tablename__ = "user"
+
+        id = Column(Integer, primary_key=True)
+        name = Column(String)
+        addresses = relationship("Address", back_populates="user")
+
+    class Address(HasPrivate, Base):
+        __tablename__ = "address"
+
+        id = Column(Integer, primary_key=True)
+        email = Column(String)
+        user_id = Column(Integer, ForeignKey("user.id"))
+
+        user = relationship("User", back_populates="addresses")
+
+    engine = create_engine("sqlite://", echo=True)
+    Base.metadata.create_all(engine)
+
+    Session = sessionmaker(bind=engine, future=True)
+
+    sess = Session()
+
+    sess.add_all(
+        [
+            User(
+                name="u1",
+                public=True,
+                addresses=[
+                    Address(email="u1a1", public=True),
+                    Address(email="u1a2", public=True),
+                ],
+            ),
+            User(
+                name="u2",
+                public=True,
+                addresses=[
+                    Address(email="u2a1", public=False),
+                    Address(email="u2a2", public=True),
+                ],
+            ),
+            User(
+                name="u3",
+                public=False,
+                addresses=[
+                    Address(email="u3a1", public=False),
+                    Address(email="u3a2", public=False),
+                ],
+            ),
+            User(
+                name="u4",
+                public=False,
+                addresses=[
+                    Address(email="u4a1", public=False),
+                    Address(email="u4a2", public=True),
+                ],
+            ),
+            User(
+                name="u5",
+                public=True,
+                addresses=[
+                    Address(email="u5a1", public=True),
+                    Address(email="u5a2", public=False),
+                ],
+            ),
+        ]
+    )
+
+    sess.commit()
+
+    # now querying Address or User objects only gives us the public ones
+    for u1 in sess.query(User).options(orm.selectinload(User.addresses)):
+        assert u1.public
+
+        # the addresses collection will also be "public only", which works
+        # for all relationship loaders including joinedload
+        for address in u1.addresses:
+            assert address.public
+
+    # works for columns too
+    cols = (
+        sess.query(User.id, Address.id)
+        .join(User.addresses)
+        .order_by(User.id, Address.id)
+        .all()
+    )
+    assert cols == [(1, 1), (1, 2), (2, 4), (5, 9)]
+
+    cols = (
+        sess.query(User.id, Address.id)
+        .join(User.addresses)
+        .order_by(User.id, Address.id)
+        .execution_options(include_private=True)
+        .all()
+    )
+    assert cols == [
+        (1, 1),
+        (1, 2),
+        (2, 3),
+        (2, 4),
+        (3, 5),
+        (3, 6),
+        (4, 7),
+        (4, 8),
+        (5, 9),
+        (5, 10),
+    ]
+
+    # count all public addresses
+    assert sess.query(Address).count() == 5
+
+    # count all addresses public and private
+    assert (
+        sess.query(Address).execution_options(include_private=True).count()
+        == 10
+    )
+
+    # load an Address that is public, but its parent User is private
+    # (2.0 style query)
+    a1 = sess.execute(select(Address).filter_by(email="u4a2")).scalar()
+
+    # assuming the User isn't already in the Session, it returns None
+    assert a1.user is None
+
+    # however, if that user is present in the session, then a many-to-one
+    # does a simple get() and it will be present
+    sess.expire(a1, ["user"])
+    u1 = sess.execute(
+        select(User)
+        .filter_by(name="u4")
+        .execution_options(include_private=True)
+    ).scalar()
+    assert a1.user is u1
diff --git a/examples/extending_query/temporal_range.py b/examples/extending_query/temporal_range.py
new file mode 100644 (file)
index 0000000..3706877
--- /dev/null
@@ -0,0 +1,136 @@
+"""Illustrates a custom per-query criteria that will be applied
+to selected entities.
+
+
+"""
+
+import datetime
+
+from sqlalchemy import Column
+from sqlalchemy import DateTime
+from sqlalchemy import orm
+
+
+class HasTemporal(object):
+    """Mixin that identifies a class as having a timestamp column"""
+
+    timestamp = Column(
+        DateTime, default=datetime.datetime.utcnow, nullable=False
+    )
+
+
+def temporal_range(range_lower, range_upper):
+    return orm.with_loader_criteria(
+        HasTemporal,
+        lambda cls: cls.timestamp.between(range_lower, range_upper),
+        include_aliases=True,
+    )
+
+
+if __name__ == "__main__":
+
+    from sqlalchemy import Integer, Column, ForeignKey
+    from sqlalchemy import select
+    from sqlalchemy import create_engine
+    from sqlalchemy.orm import relationship, sessionmaker, selectinload
+    from sqlalchemy.ext.declarative import declarative_base
+
+    Base = declarative_base()
+
+    class Parent(HasTemporal, Base):
+        __tablename__ = "parent"
+        id = Column(Integer, primary_key=True)
+        children = relationship("Child")
+
+    class Child(HasTemporal, Base):
+        __tablename__ = "child"
+        id = Column(Integer, primary_key=True)
+        parent_id = Column(Integer, ForeignKey("parent.id"), nullable=False)
+
+    engine = create_engine("sqlite://", echo=True)
+    Base.metadata.create_all(engine)
+
+    Session = sessionmaker(bind=engine, future=True)
+
+    sess = Session()
+
+    c1, c2, c3, c4, c5 = [
+        Child(timestamp=datetime.datetime(2009, 10, 15, 12, 00, 00)),
+        Child(timestamp=datetime.datetime(2009, 10, 17, 12, 00, 00)),
+        Child(timestamp=datetime.datetime(2009, 10, 20, 12, 00, 00)),
+        Child(timestamp=datetime.datetime(2009, 10, 12, 12, 00, 00)),
+        Child(timestamp=datetime.datetime(2009, 10, 17, 12, 00, 00)),
+    ]
+
+    p1 = Parent(
+        timestamp=datetime.datetime(2009, 10, 15, 12, 00, 00),
+        children=[c1, c2, c3],
+    )
+    p2 = Parent(
+        timestamp=datetime.datetime(2009, 10, 17, 12, 00, 00),
+        children=[c4, c5],
+    )
+
+    sess.add_all([p1, p2])
+    sess.commit()
+
+    # use populate_existing() to ensure the range option takes
+    # place for elements already in the identity map
+
+    parents = (
+        sess.query(Parent)
+        .populate_existing()
+        .options(
+            temporal_range(
+                datetime.datetime(2009, 10, 16, 12, 00, 00),
+                datetime.datetime(2009, 10, 18, 12, 00, 00),
+            )
+        )
+        .all()
+    )
+
+    assert parents[0] == p2
+    assert parents[0].children == [c5]
+
+    sess.expire_all()
+
+    # try it with eager load
+    parents = (
+        sess.query(Parent)
+        .options(
+            temporal_range(
+                datetime.datetime(2009, 10, 16, 12, 00, 00),
+                datetime.datetime(2009, 10, 18, 12, 00, 00),
+            )
+        )
+        .options(selectinload(Parent.children))
+        .all()
+    )
+
+    assert parents[0] == p2
+    assert parents[0].children == [c5]
+
+    sess.expire_all()
+
+    # illustrate a 2.0 style query
+    print("------------------")
+    parents = (
+        sess.execute(
+            select(Parent)
+            .execution_options(populate_existing=True)
+            .options(
+                temporal_range(
+                    datetime.datetime(2009, 10, 15, 11, 00, 00),
+                    datetime.datetime(2009, 10, 18, 12, 00, 00),
+                )
+            )
+            .join(Parent.children)
+            .filter(Child.id == 2)
+        )
+        .scalars()
+        .all()
+    )
+
+    assert parents[0] == p1
+    print("-------------------")
+    assert parents[0].children == [c1, c2]
index eb8e10686fb188938630fa3c1e7917e316780af0..90cf6cc6c41e851cef1ef1259d24e5892f39ef42 100644 (file)
@@ -4,21 +4,28 @@ databases.
 
 The basic components of a "sharded" mapping are:
 
-* multiple databases, each assigned a 'shard id'
+* multiple :class:`_engine.Engine` instances, each assigned a "shard id".
+  These :class:`_engine.Engine` instances may refer to different databases,
+  or different schemas / accounts within the same database, or they can
+  even be differentiated only by options that will cause them to access
+  different schemas or tables when used.
+
 * a function which can return a single shard id, given an instance
   to be saved; this is called "shard_chooser"
+
 * a function which can return a list of shard ids which apply to a particular
   instance identifier; this is called "id_chooser".If it returns all shard ids,
   all shards will be searched.
+
 * a function which can return a list of shard ids to try, given a particular
   Query ("query_chooser").  If it returns all shard ids, all shards will be
   queried and the results joined together.
 
-In this example, four sqlite databases will store information about weather
-data on a database-per-continent basis. We provide example shard_chooser,
-id_chooser and query_chooser functions. The query_chooser illustrates
-inspection of the SQL expression element in order to attempt to determine a
-single shard being requested.
+In these examples, different kinds of shards are used against the same basic
+example which accommodates weather data on a per-continent basis. We provide
+example shard_chooser, id_chooser and query_chooser functions. The
+query_chooser illustrates inspection of the SQL expression element in order to
+attempt to determine a single shard being requested.
 
 The construction of generic sharding routines is an ambitious approach
 to the issue of organizing instances among multiple databases.   For a
similarity index 79%
rename from examples/sharding/attribute_shard.py
rename to examples/sharding/separate_databases.py
index 7b8f87d90b64ea192340d6b9b764a0486df15b79..95f12fa722d918e91dc03db9c552b9bbb8da55b6 100644 (file)
@@ -1,3 +1,5 @@
+"""Illustrates sharding using distinct SQLite databases."""
+
 import datetime
 
 from sqlalchemy import Column
@@ -7,6 +9,7 @@ from sqlalchemy import Float
 from sqlalchemy import ForeignKey
 from sqlalchemy import inspect
 from sqlalchemy import Integer
+from sqlalchemy import select
 from sqlalchemy import String
 from sqlalchemy import Table
 from sqlalchemy.ext.declarative import declarative_base
@@ -26,15 +29,15 @@ db4 = create_engine("sqlite://", echo=echo)
 
 # create session function.  this binds the shard ids
 # to databases within a ShardedSession and returns it.
-create_session = sessionmaker(class_=ShardedSession)
-
-create_session.configure(
+Session = sessionmaker(
+    class_=ShardedSession,
+    future=True,
     shards={
         "north_america": db1,
         "asia": db2,
         "europe": db3,
         "south_america": db4,
-    }
+    },
 )
 
 
@@ -54,7 +57,7 @@ ids = Table("ids", Base.metadata, Column("nextid", Integer, nullable=False))
 def id_generator(ctx):
     # in reality, might want to use a separate transaction for this.
     with db1.connect() as conn:
-        nextid = conn.scalar(ids.select(for_update=True))
+        nextid = conn.scalar(ids.select().with_for_update())
         conn.execute(ids.update(values={ids.c.nextid: ids.c.nextid + 1}))
     return nextid
 
@@ -99,11 +102,11 @@ class Report(Base):
 
 # create tables
 for db in (db1, db2, db3, db4):
-    Base.metadata.drop_all(db)
     Base.metadata.create_all(db)
 
 # establish initial "id" in db1
-db1.execute(ids.insert(), nextid=1)
+with db1.begin() as conn:
+    conn.execute(ids.insert(), nextid=1)
 
 
 # step 5. define sharding functions.
@@ -199,18 +202,7 @@ def _get_query_comparisons(query):
     def visit_bindparam(bind):
         # visit a bind parameter.
 
-        # check in _params for it first
-        if bind.key in query._params:
-            value = query._params[bind.key]
-        elif bind.callable:
-            # some ORM functions (lazy loading)
-            # place the bind's value as a
-            # callable for deferred evaluation.
-            value = bind.callable()
-        else:
-            # just use .value
-            value = bind.value
-
+        value = bind.effective_value
         binds[bind] = value
 
     def visit_column(column):
@@ -230,9 +222,9 @@ def _get_query_comparisons(query):
     # here we will traverse through the query's criterion, searching
     # for SQL constructs.  We will place simple column comparisons
     # into a list.
-    if query._criterion is not None:
-        visitors.traverse_depthfirst(
-            query._criterion,
+    if query.whereclause is not None:
+        visitors.traverse(
+            query.whereclause,
             {},
             {
                 "bindparam": visit_bindparam,
@@ -244,7 +236,7 @@ def _get_query_comparisons(query):
 
 
 # further configure create_session to use these functions
-create_session.configure(
+Session.configure(
     shard_chooser=shard_chooser,
     id_chooser=id_chooser,
     query_chooser=query_chooser,
@@ -264,36 +256,47 @@ tokyo.reports.append(Report(80.0))
 newyork.reports.append(Report(75))
 quito.reports.append(Report(85))
 
-sess = create_session()
+with Session() as sess:
 
-sess.add_all([tokyo, newyork, toronto, london, dublin, brasilia, quito])
+    sess.add_all([tokyo, newyork, toronto, london, dublin, brasilia, quito])
 
-sess.commit()
+    sess.commit()
 
-t = sess.query(WeatherLocation).get(tokyo.id)
-assert t.city == tokyo.city
-assert t.reports[0].temperature == 80.0
+    t = sess.get(WeatherLocation, tokyo.id)
+    assert t.city == tokyo.city
+    assert t.reports[0].temperature == 80.0
 
-north_american_cities = sess.query(WeatherLocation).filter(
-    WeatherLocation.continent == "North America"
-)
-assert {c.city for c in north_american_cities} == {"New York", "Toronto"}
+    north_american_cities = sess.execute(
+        select(WeatherLocation).filter(
+            WeatherLocation.continent == "North America"
+        )
+    ).scalars()
 
-asia_and_europe = sess.query(WeatherLocation).filter(
-    WeatherLocation.continent.in_(["Europe", "Asia"])
-)
-assert {c.city for c in asia_and_europe} == {"Tokyo", "London", "Dublin"}
+    assert {c.city for c in north_american_cities} == {"New York", "Toronto"}
+
+    asia_and_europe = sess.execute(
+        select(WeatherLocation).filter(
+            WeatherLocation.continent.in_(["Europe", "Asia"])
+        )
+    ).scalars()
 
-# the Report class uses a simple integer primary key.  So across two databases,
-# a primary key will be repeated.  The "identity_token" tracks in memory
-# that these two identical primary keys are local to different databases.
-newyork_report = newyork.reports[0]
-tokyo_report = tokyo.reports[0]
+    assert {c.city for c in asia_and_europe} == {"Tokyo", "London", "Dublin"}
 
-assert inspect(newyork_report).identity_key == (Report, (1,), "north_america")
-assert inspect(tokyo_report).identity_key == (Report, (1,), "asia")
+    # the Report class uses a simple integer primary key.  So across two
+    # databases, a primary key will be repeated.  The "identity_token" tracks
+    # in memory that these two identical primary keys are local to different
+    # databases.
+    newyork_report = newyork.reports[0]
+    tokyo_report = tokyo.reports[0]
+
+    assert inspect(newyork_report).identity_key == (
+        Report,
+        (1,),
+        "north_america",
+    )
+    assert inspect(tokyo_report).identity_key == (Report, (1,), "asia")
 
-# the token representing the originating shard is also available directly
+    # the token representing the originating shard is also available directly
 
-assert inspect(newyork_report).identity_token == "north_america"
-assert inspect(tokyo_report).identity_token == "asia"
+    assert inspect(newyork_report).identity_token == "north_america"
+    assert inspect(tokyo_report).identity_token == "asia"
diff --git a/examples/sharding/separate_tables.py b/examples/sharding/separate_tables.py
new file mode 100644 (file)
index 0000000..f24dde2
--- /dev/null
@@ -0,0 +1,316 @@
+"""Illustrates sharding using a single SQLite database, that will however
+have multiple tables using a naming convention."""
+
+import datetime
+
+from sqlalchemy import Column
+from sqlalchemy import create_engine
+from sqlalchemy import DateTime
+from sqlalchemy import event
+from sqlalchemy import Float
+from sqlalchemy import ForeignKey
+from sqlalchemy import inspect
+from sqlalchemy import Integer
+from sqlalchemy import select
+from sqlalchemy import String
+from sqlalchemy import Table
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.ext.horizontal_shard import ShardedSession
+from sqlalchemy.orm import relationship
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.sql import operators
+from sqlalchemy.sql import visitors
+
+
+echo = True
+engine = create_engine("sqlite://", echo=echo)
+
+db1 = engine.execution_options(table_prefix="north_america")
+db2 = engine.execution_options(table_prefix="asia")
+db3 = engine.execution_options(table_prefix="europe")
+db4 = engine.execution_options(table_prefix="south_america")
+
+
+@event.listens_for(engine, "before_cursor_execute", retval=True)
+def before_cursor_execute(
+    conn, cursor, statement, parameters, context, executemany
+):
+    table_prefix = context.execution_options.get("table_prefix", None)
+    if table_prefix:
+        statement = statement.replace("_prefix_", table_prefix)
+    return statement, parameters
+
+
+# create session function.  this binds the shard ids
+# to databases within a ShardedSession and returns it.
+Session = sessionmaker(
+    class_=ShardedSession,
+    future=True,
+    shards={
+        "north_america": db1,
+        "asia": db2,
+        "europe": db3,
+        "south_america": db4,
+    },
+)
+
+
+# mappings and tables
+Base = declarative_base()
+
+# we need a way to create identifiers which are unique across all databases.
+# one easy way would be to just use a composite primary key, where  one value
+# is the shard id.  but here, we'll show something more "generic", an id
+# generation function.  we'll use a simplistic "id table" stored in database
+# #1.  Any other method will do just as well; UUID, hilo, application-specific,
+# etc.
+
+ids = Table("ids", Base.metadata, Column("nextid", Integer, nullable=False))
+
+
+def id_generator(ctx):
+    # in reality, might want to use a separate transaction for this.
+    with engine.connect() as conn:
+        nextid = conn.scalar(ids.select().with_for_update())
+        conn.execute(ids.update(values={ids.c.nextid: ids.c.nextid + 1}))
+    return nextid
+
+
+# table setup.  we'll store a lead table of continents/cities, and a secondary
+# table storing locations. a particular row will be placed in the database
+# whose shard id corresponds to the 'continent'.  in this setup, secondary rows
+# in 'weather_reports' will be placed in the same DB as that of the parent, but
+# this can be changed if you're willing to write more complex sharding
+# functions.
+
+
+class WeatherLocation(Base):
+    __tablename__ = "_prefix__weather_locations"
+
+    id = Column(Integer, primary_key=True, default=id_generator)
+    continent = Column(String(30), nullable=False)
+    city = Column(String(50), nullable=False)
+
+    reports = relationship("Report", backref="location")
+
+    def __init__(self, continent, city):
+        self.continent = continent
+        self.city = city
+
+
+class Report(Base):
+    __tablename__ = "_prefix__weather_reports"
+
+    id = Column(Integer, primary_key=True)
+    location_id = Column(
+        "location_id", Integer, ForeignKey("_prefix__weather_locations.id")
+    )
+    temperature = Column("temperature", Float)
+    report_time = Column(
+        "report_time", DateTime, default=datetime.datetime.now
+    )
+
+    def __init__(self, temperature):
+        self.temperature = temperature
+
+
+# create tables
+for db in (db1, db2, db3, db4):
+    Base.metadata.create_all(db)
+
+# establish initial "id" in db1
+with db1.begin() as conn:
+    conn.execute(ids.insert(), nextid=1)
+
+
+# step 5. define sharding functions.
+
+# we'll use a straight mapping of a particular set of "country"
+# attributes to shard id.
+shard_lookup = {
+    "North America": "north_america",
+    "Asia": "asia",
+    "Europe": "europe",
+    "South America": "south_america",
+}
+
+
+def shard_chooser(mapper, instance, clause=None):
+    """shard chooser.
+
+    looks at the given instance and returns a shard id
+    note that we need to define conditions for
+    the WeatherLocation class, as well as our secondary Report class which will
+    point back to its WeatherLocation via its 'location' attribute.
+
+    """
+    if isinstance(instance, WeatherLocation):
+        return shard_lookup[instance.continent]
+    else:
+        return shard_chooser(mapper, instance.location)
+
+
+def id_chooser(query, ident):
+    """id chooser.
+
+    given a primary key, returns a list of shards
+    to search.  here, we don't have any particular information from a
+    pk so we just return all shard ids. often, you'd want to do some
+    kind of round-robin strategy here so that requests are evenly
+    distributed among DBs.
+
+    """
+    if query.lazy_loaded_from:
+        # if we are in a lazy load, we can look at the parent object
+        # and limit our search to that same shard, assuming that's how we've
+        # set things up.
+        return [query.lazy_loaded_from.identity_token]
+    else:
+        return ["north_america", "asia", "europe", "south_america"]
+
+
+def query_chooser(query):
+    """query chooser.
+
+    this also returns a list of shard ids, which can
+    just be all of them.  but here we'll search into the Query in order
+    to try to narrow down the list of shards to query.
+
+    """
+    ids = []
+
+    # we'll grab continent names as we find them
+    # and convert to shard ids
+    for column, operator, value in _get_query_comparisons(query):
+        # "shares_lineage()" returns True if both columns refer to the same
+        # statement column, adjusting for any annotations present.
+        # (an annotation is an internal clone of a Column object
+        # and occur when using ORM-mapped attributes like
+        # "WeatherLocation.continent"). A simpler comparison, though less
+        # accurate, would be "column.key == 'continent'".
+        if column.shares_lineage(WeatherLocation.__table__.c.continent):
+            if operator == operators.eq:
+                ids.append(shard_lookup[value])
+            elif operator == operators.in_op:
+                ids.extend(shard_lookup[v] for v in value)
+
+    if len(ids) == 0:
+        return ["north_america", "asia", "europe", "south_america"]
+    else:
+        return ids
+
+
+def _get_query_comparisons(query):
+    """Search an orm.Query object for binary expressions.
+
+    Returns expressions which match a Column against one or more
+    literal values as a list of tuples of the form
+    (column, operator, values).   "values" is a single value
+    or tuple of values depending on the operator.
+
+    """
+    binds = {}
+    clauses = set()
+    comparisons = []
+
+    def visit_bindparam(bind):
+        # visit a bind parameter.
+
+        value = bind.effective_value
+        binds[bind] = value
+
+    def visit_column(column):
+        clauses.add(column)
+
+    def visit_binary(binary):
+        if binary.left in clauses and binary.right in binds:
+            comparisons.append(
+                (binary.left, binary.operator, binds[binary.right])
+            )
+
+        elif binary.left in binds and binary.right in clauses:
+            comparisons.append(
+                (binary.right, binary.operator, binds[binary.left])
+            )
+
+    # here we will traverse through the query's criterion, searching
+    # for SQL constructs.  We will place simple column comparisons
+    # into a list.
+    if query.whereclause is not None:
+        visitors.traverse(
+            query.whereclause,
+            {},
+            {
+                "bindparam": visit_bindparam,
+                "binary": visit_binary,
+                "column": visit_column,
+            },
+        )
+    return comparisons
+
+
+# further configure create_session to use these functions
+Session.configure(
+    shard_chooser=shard_chooser,
+    id_chooser=id_chooser,
+    query_chooser=query_chooser,
+)
+
+# save and load objects!
+
+tokyo = WeatherLocation("Asia", "Tokyo")
+newyork = WeatherLocation("North America", "New York")
+toronto = WeatherLocation("North America", "Toronto")
+london = WeatherLocation("Europe", "London")
+dublin = WeatherLocation("Europe", "Dublin")
+brasilia = WeatherLocation("South America", "Brasila")
+quito = WeatherLocation("South America", "Quito")
+
+tokyo.reports.append(Report(80.0))
+newyork.reports.append(Report(75))
+quito.reports.append(Report(85))
+
+with Session() as sess:
+
+    sess.add_all([tokyo, newyork, toronto, london, dublin, brasilia, quito])
+
+    sess.commit()
+
+    t = sess.get(WeatherLocation, tokyo.id)
+    assert t.city == tokyo.city
+    assert t.reports[0].temperature == 80.0
+
+    north_american_cities = sess.execute(
+        select(WeatherLocation).filter(
+            WeatherLocation.continent == "North America"
+        )
+    ).scalars()
+
+    assert {c.city for c in north_american_cities} == {"New York", "Toronto"}
+
+    asia_and_europe = sess.execute(
+        select(WeatherLocation).filter(
+            WeatherLocation.continent.in_(["Europe", "Asia"])
+        )
+    ).scalars()
+
+    assert {c.city for c in asia_and_europe} == {"Tokyo", "London", "Dublin"}
+
+    # the Report class uses a simple integer primary key.  So across two
+    # databases, a primary key will be repeated.  The "identity_token" tracks
+    # in memory that these two identical primary keys are local to different
+    # databases.
+    newyork_report = newyork.reports[0]
+    tokyo_report = tokyo.reports[0]
+
+    assert inspect(newyork_report).identity_key == (
+        Report,
+        (1,),
+        "north_america",
+    )
+    assert inspect(tokyo_report).identity_key == (Report, (1,), "asia")
+
+    # the token representing the originating shard is also available directly
+
+    assert inspect(newyork_report).identity_token == "north_america"
+    assert inspect(tokyo_report).identity_token == "asia"
index f925df6c53b22fce8674e8db838313a67f495620..1baef29afcbc5a82cc3958f3a48d194d79ace084 100644 (file)
@@ -953,7 +953,7 @@ class CreateEnginePlugin(object):
         engine = create_engine(
           "mysql+pymysql://scott:tiger@localhost/test?plugin=myplugin")
 
-    Alternatively, the :paramref:`.create_engine.plugins" argument may be
+    Alternatively, the :paramref:`_sa.create_engine.plugins" argument may be
     passed as a list to :func:`_sa.create_engine`::
 
         engine = create_engine(
index db546380e63e7b19e6d4e9ef8d80c732b12234a6..621cba6742f3290c857aad329d022e9d8d1e2405 100644 (file)
@@ -649,6 +649,11 @@ class Result(InPlaceGenerative):
         it will produce a new :class:`_engine.Result` object each time
         against its stored set of rows.
 
+        .. seealso::
+
+            :ref:`do_orm_execute_re_executing` - example usage within the
+            ORM to implement a result-set cache.
+
         """
         return FrozenResult(self)
 
@@ -1173,12 +1178,28 @@ class FrozenResult(object):
 
         frozen = result.freeze()
 
-        r1 = frozen()
-        r2 = frozen()
+        unfrozen_result_one = frozen()
+
+        for row in unfrozen_result_one:
+            print(row)
+
+        unfrozen_result_two = frozen()
+        rows = unfrozen_result_two.all()
+
         # ... etc
 
     .. versionadded:: 1.4
 
+    .. seealso::
+
+        .. seealso::
+
+            :ref:`do_orm_execute_re_executing` - example usage within the
+            ORM to implement a result-set cache.
+
+            :func:`_orm.loading.merge_frozen_result` - ORM function to merge
+            a frozen result back into a :class:`_orm.Session`.
+
     """
 
     def __init__(self, result):
index 6bd63ceca6f9e74b571bce5ab85269fb64819c84..b36c448ceb3194fafd15503ade8f70b98a50fb5c 100644 (file)
@@ -61,9 +61,6 @@ def listen(target, identifier, fn, *args, **kw):
 
         event.listen(Mapper, "before_configure", on_config, once=True)
 
-    .. versionadded:: 0.9.4 Added ``once=True`` to :func:`.event.listen`
-       and :func:`.event.listens_for`.
-
     .. warning:: The ``once`` argument does not imply automatic de-registration
        of the listener function after it has been invoked a first time; a
        listener entry will remain associated with the target object.
@@ -128,9 +125,6 @@ def listens_for(target, identifier, *args, **kw):
             do_config()
 
 
-    .. versionadded:: 0.9.4 Added ``once=True`` to :func:`.event.listen`
-       and :func:`.event.listens_for`.
-
     .. warning:: The ``once`` argument does not imply automatic de-registration
        of the listener function after it has been invoked a first time; a
        listener entry will remain associated with the target object.
@@ -173,8 +167,6 @@ def remove(target, identifier, fn):
     propagated to subclasses of ``SomeMappedClass``; the :func:`.remove`
     function will revert all of these operations.
 
-    .. versionadded:: 0.9.0
-
     .. note::
 
         The :func:`.remove` function cannot be called at the same time
@@ -206,8 +198,6 @@ def remove(target, identifier, fn):
 def contains(target, identifier, fn):
     """Return True if the given target/ident/fn is set up to listen.
 
-    .. versionadded:: 0.9.0
-
     """
 
     return _event_key(target, identifier, fn).contains()
index f63c7d101c7e60f269756bdd99ab20046397569c..14115d3775063ff63eb43ba3c8d66704605cc9e1 100644 (file)
@@ -96,8 +96,7 @@ def _standard_listen_example(dispatch_collection, sample_target, fn):
     else:
         current_since = None
     text = (
-        "from sqlalchemy import event\n\n"
-        "# standard decorator style%(current_since)s\n"
+        "from sqlalchemy import event\n\n\n"
         "@event.listens_for(%(sample_target)s, '%(event_name)s')\n"
         "def receive_%(event_name)s("
         "%(named_event_arguments)s%(has_kw_arguments)s):\n"
@@ -105,17 +104,6 @@ def _standard_listen_example(dispatch_collection, sample_target, fn):
         "\n    # ... (event handling logic) ...\n"
     )
 
-    if len(dispatch_collection.arg_names) > 3:
-        text += (
-            "\n# named argument style (new in 0.9)\n"
-            "@event.listens_for("
-            "%(sample_target)s, '%(event_name)s', named=True)\n"
-            "def receive_%(event_name)s(**kw):\n"
-            "    \"listen for the '%(event_name)s' event\"\n"
-            "%(example_kw_arg)s\n"
-            "\n    # ... (event handling logic) ...\n"
-        )
-
     text %= {
         "current_since": " (arguments as of %s)" % current_since
         if current_since
index 4581038389f3792c1f626306beb014f3671bfb17..199ae11e5147a53628f4666d19bed2795254dc3f 100644 (file)
@@ -272,6 +272,14 @@ def eagerload(*args, **kwargs):
 
 contains_alias = public_factory(AliasOption, ".orm.contains_alias")
 
+if True:
+    from .events import AttributeEvents  # noqa
+    from .events import MapperEvents  # noqa
+    from .events import InstanceEvents  # noqa
+    from .events import InstrumentationEvents  # noqa
+    from .events import QueryEvents  # noqa
+    from .events import SessionEvents  # noqa
+
 
 def __go(lcls):
     global __all__
index 217aa76c75f8c700bc5300ef51a0623119234ff8..ec907c63e7d69169d00c8f6c99bfdc14f102fda7 100644 (file)
@@ -1427,7 +1427,27 @@ class SessionEvents(event.Events):
 
         .. seealso::
 
-            :class:`.ORMExecuteState`
+            :ref:`session_execute_events` - top level documentation on how
+            to use :meth:`_orm.SessionEvents.do_orm_execute`
+
+            :class:`.ORMExecuteState` - the object passed to the
+            :meth:`_orm.SessionEvents.do_orm_execute` event which contains
+            all information about the statement to be invoked.  It also
+            provides an interface to extend the current statement, options,
+            and parameters as well as an option that allows programmatic
+            invocation of the statement at any point.
+
+            :ref:`examples_session_orm_events` - includes examples of using
+            :meth:`_orm.SessionEvents.do_orm_execute`
+
+            :ref:`examples_caching` - an example of how to integrate
+            Dogpile caching with the ORM :class:`_orm.Session` making use
+            of the :meth:`_orm.SessionEvents.do_orm_execute` event hook.
+
+            :ref:`examples_sharding` - the Horizontal Sharding example /
+            extension relies upon the
+            :meth:`_orm.SessionEvents.do_orm_execute` event hook to invoke a
+            SQL statement on multiple backends and return a merged result.
 
 
         .. versionadded:: 1.4
@@ -2585,12 +2605,8 @@ class QueryEvents(event.Events):
     """Represent events within the construction of a :class:`_query.Query`
     object.
 
-    The events here are intended to be used with an as-yet-unreleased
-    inspection system for :class:`_query.Query`.   Some very basic operations
-    are possible now, however the inspection system is intended to allow
-    complex query manipulations to be automated.
-
-    .. versionadded:: 1.0.0
+    The :class:`_orm.QueryEvents` hooks are now superseded by the
+    :meth:`_orm.SessionEvents.do_orm_execute` event hook.
 
     """
 
@@ -2602,6 +2618,17 @@ class QueryEvents(event.Events):
         object before it is composed into a
         core :class:`_expression.Select` object.
 
+        .. deprecated:: 1.4  The :meth:`_orm.QueryEvents.before_compile` event
+           is superseded by the much more capable
+           :meth:`_orm.SessionEvents.do_orm_execute` hook.   In version 1.4,
+           the :meth:`_orm.QueryEvents.before_compile` event is **no longer
+           used** for ORM-level attribute loads, such as loads of deferred
+           or expired attributes as well as relationship loaders.   See the
+           new examples in :ref:`examples_session_orm_events` which
+           illustrate new ways of intercepting and modifying ORM queries
+           for the most common purpose of adding arbitrary filter criteria.
+
+
         This event is intended to allow changes to the query given::
 
             @event.listens_for(Query, "before_compile", retval=True)
@@ -2656,6 +2683,10 @@ class QueryEvents(event.Events):
         """Allow modifications to the :class:`_query.Query` object within
         :meth:`_query.Query.update`.
 
+        .. deprecated:: 1.4  The :meth:`_orm.QueryEvents.before_compile_update`
+           event is superseded by the much more capable
+           :meth:`_orm.SessionEvents.do_orm_execute` hook.
+
         Like the :meth:`.QueryEvents.before_compile` event, if the event
         is to be used to alter the :class:`_query.Query` object, it should
         be configured with ``retval=True``, and the modified
@@ -2702,6 +2733,10 @@ class QueryEvents(event.Events):
         """Allow modifications to the :class:`_query.Query` object within
         :meth:`_query.Query.delete`.
 
+        .. deprecated:: 1.4  The :meth:`_orm.QueryEvents.before_compile_delete`
+           event is superseded by the much more capable
+           :meth:`_orm.SessionEvents.do_orm_execute` hook.
+
         Like the :meth:`.QueryEvents.before_compile` event, this event
         should be configured with ``retval=True``, and the modified
         :class:`_query.Query` object returned, as in ::
index 60139315661c0928117d6c78db5b4e5a02c39cfe..2eb3e1368992c1238dcf28d2ff597fccde791b88 100644 (file)
@@ -141,6 +141,21 @@ def instances(cursor, context):
 
 @util.preload_module("sqlalchemy.orm.context")
 def merge_frozen_result(session, statement, frozen_result, load=True):
+    """Merge a :class:`_engine.FrozenResult` back into a :class:`_orm.Session`,
+    returning a new :class:`_engine.Result` object with :term:`persistent`
+    objects.
+
+    See the section :ref:`do_orm_execute_re_executing` for an example.
+
+    .. seealso::
+
+        :ref:`do_orm_execute_re_executing`
+
+        :meth:`_engine.Result.freeze`
+
+        :class:`_engine.FrozenResult`
+
+    """
     querycontext = util.preloaded.orm_context
 
     if load:
@@ -184,6 +199,11 @@ def merge_frozen_result(session, statement, frozen_result, load=True):
         session.autoflush = autoflush
 
 
+@util.deprecated(
+    "2.0",
+    "The :func:`_orm.merge_result` method is superseded by the "
+    ":func:`_orm.merge_frozen_result` function.",
+)
 @util.preload_module("sqlalchemy.orm.context")
 def merge_result(query, iterator, load=True):
     """Merge a result into this :class:`.Query` object's Session."""
index e9d4ac2c67a2eef7d3d6f188dd03b9f6958f31ec..ed1af0a803ff8529e83dc55908acc9dbfe958978 100644 (file)
@@ -107,6 +107,10 @@ class ORMExecuteState(util.MemoizedSlots):
 
     .. versionadded:: 1.4
 
+    .. seealso::
+
+        :ref:`session_execute_events` - top level documentation on how
+        to use :meth:`_orm.SessionEvents.do_orm_execute`
 
     """
 
@@ -158,14 +162,24 @@ class ORMExecuteState(util.MemoizedSlots):
         bind_arguments=None,
     ):
         """Execute the statement represented by this
-        :class:`.ORMExecuteState`, without re-invoking events.
-
-        This method essentially performs a re-entrant execution of the
-        current statement for which the :meth:`.SessionEvents.do_orm_execute`
-        event is being currently invoked.    The use case for this is
-        for event handlers that want to override how the ultimate results
-        object is returned, such as for schemes that retrieve results from
-        an offline cache or which concatenate results from multiple executions.
+        :class:`.ORMExecuteState`, without re-invoking events that have
+        already proceeded.
+
+        This method essentially performs a re-entrant execution of the current
+        statement for which the :meth:`.SessionEvents.do_orm_execute` event is
+        being currently invoked.    The use case for this is for event handlers
+        that want to override how the ultimate
+        :class:`_engine.Result` object is returned, such as for schemes that
+        retrieve results from an offline cache or which concatenate results
+        from multiple executions.
+
+        When the :class:`_engine.Result` object is returned by the actual
+        handler function within :meth:`_orm.SessionEvents.do_orm_execute` and
+        is propagated to the calling
+        :meth:`_orm.Session.execute` method, the remainder of the
+        :meth:`_orm.Session.execute` method is preempted and the
+        :class:`_engine.Result` object is returned to the caller of
+        :meth:`_orm.Session.execute` immediately.
 
         :param statement: optional statement to be invoked, in place of the
          statement currently represented by :attr:`.ORMExecuteState.statement`.
@@ -970,13 +984,30 @@ class Session(_SessionClassMethods):
            transaction will load from the most recent database state.
 
         :param future: if True, use 2.0 style behavior for the
-          :meth:`_orm.Session.execute` method.  This includes that the
-          :class:`_engine.Result` object returned will return new-style
-          tuple rows, as well as that Core constructs such as
-          :class:`_sql.Select`,
-          :class:`_sql.Update` and :class:`_sql.Delete` will be interpreted
-          in an ORM context if they are made against ORM entities rather than
-          plain :class:`.Table` metadata objects.
+          :meth:`_orm.Session.execute` method.   Future mode includes the
+          following behaviors:
+
+          * The :class:`_engine.Result` object returned by the
+            :meth:`_orm.Session.execute` method will return new-style tuple
+            :class:`_engine.Row` objects
+
+          * The :meth:`_orm.Session.execute` method will invoke ORM style
+            queries given objects like :class:`_sql.Select`,
+            :class:`_sql.Update` and :class:`_sql.Delete` against ORM entities
+
+          * The :class:`_orm.Session` will not use "bound" metadata in order
+            to locate an :class:`_engine.Engine`; the engine or engines in use
+            must be specified to the constructor of :class:`_orm.Session` or
+            otherwise be configured against the :class:`_orm.sessionmaker`
+            in use
+
+          * The "subtransactions" feature of :meth:`_orm.Session.begin` is
+            removed in version 2.0 and is disabled when the future flag is
+            set.
+
+          * The behavior of the :paramref:`_orm.relationship.cascade_backrefs`
+            flag on a :func:`_orm.relationship` will always assume
+            "False" behavior.
 
           The "future" flag is also available on a per-execution basis
           using the :paramref:`_orm.Session.execute.future` flag.
index bba0cc16c77ac04699072ec3c0bce2d83d2796a3..d5dece9a6c9e6a516e6aee5f9519809395ca2321 100644 (file)
@@ -187,6 +187,15 @@ class ImmutableDictTest(fixtures.TestBase):
         eq_(d, {1: 2, 3: 4})
         eq_(d2, {1: 2, 3: 5, 7: 12})
 
+    def _dont_test_union_kw(self):
+        d = util.immutabledict({"a": "b", "c": "d"})
+
+        d2 = d.union(e="f", g="h")
+        assert isinstance(d2, util.immutabledict)
+
+        eq_(d, {"a": "b", "c": "d"})
+        eq_(d2, {"a": "b", "c": "d", "e": "f", "g": "h"})
+
     def test_union_tuples(self):
         d = util.immutabledict({1: 2, 3: 4})