1=============================
2What's new in SQLAlchemy 0.4?
3=============================
4
5.. admonition:: About this Document
6
7    This document describes changes between SQLAlchemy version 0.3,
8    last released October 14, 2007, and SQLAlchemy version 0.4,
9    last released October 12, 2008.
10
11    Document date:  March 21, 2008
12
13First Things First
14==================
15
16If you're using any ORM features, make sure you import from
17``sqlalchemy.orm``:
18
19::
20
21    from sqlalchemy import *
22    from sqlalchemy.orm import *
23
24Secondly, anywhere you used to say ``engine=``,
25``connectable=``, ``bind_to=``, ``something.engine``,
26``metadata.connect()``, use ``bind``:
27
28::
29
30    myengine = create_engine('sqlite://')
31
32    meta = MetaData(myengine)
33
34    meta2 = MetaData()
35    meta2.bind = myengine
36
37    session = create_session(bind=myengine)
38
39    statement = select([table], bind=myengine)
40
41Got those ?  Good!  You're now (95%) 0.4 compatible.  If
42you're using 0.3.10, you can make these changes immediately;
43they'll work there too.
44
45Module Imports
46==============
47
48In 0.3, "``from sqlachemy import *``" would import all of
49sqlachemy's sub-modules into your namespace. Version 0.4 no
50longer imports sub-modules into the namespace. This may mean
51you need to add extra imports into your code.
52
53In 0.3, this code worked:
54
55::
56
57    from sqlalchemy import *
58
59    class UTCDateTime(types.TypeDecorator):
60        pass
61
62In 0.4, one must do:
63
64::
65
66    from sqlalchemy import *
67    from sqlalchemy import types
68
69    class UTCDateTime(types.TypeDecorator):
70        pass
71
72Object Relational Mapping
73=========================
74
75Querying
76--------
77
78New Query API
79^^^^^^^^^^^^^
80
81Query is standardized on the generative interface (old
82interface is still there, just deprecated).   While most of
83the generative interface is available in 0.3, the 0.4 Query
84has the inner guts to match the generative outside, and has
85a lot more tricks.  All result narrowing is via ``filter()``
86and ``filter_by()``, limiting/offset is either through array
87slices or ``limit()``/``offset()``, joining is via
88``join()`` and ``outerjoin()`` (or more manually, through
89``select_from()`` as well as manually-formed criteria).
90
91To avoid deprecation warnings, you must make some changes to
92your 03 code
93
94User.query.get_by( \**kwargs )
95
96::
97
98    User.query.filter_by(**kwargs).first()
99
100User.query.select_by( \**kwargs )
101
102::
103
104    User.query.filter_by(**kwargs).all()
105
106User.query.select()
107
108::
109
110    User.query.filter(xxx).all()
111
112New Property-Based Expression Constructs
113^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
114
115By far the most palpable difference within the ORM is that
116you can now construct your query criterion using class-based
117attributes directly.  The ".c." prefix is no longer needed
118when working with mapped classes:
119
120::
121
122    session.query(User).filter(and_(User.name == 'fred', User.id > 17))
123
124While simple column-based comparisons are no big deal, the
125class attributes have some new "higher level" constructs
126available, including what was previously only available in
127``filter_by()``:
128
129::
130
131    # comparison of scalar relations to an instance
132    filter(Address.user == user)
133
134    # return all users who contain a particular address
135    filter(User.addresses.contains(address))
136
137    # return all users who *dont* contain the address
138    filter(~User.address.contains(address))
139
140    # return all users who contain a particular address with
141    # the email_address like '%foo%'
142    filter(User.addresses.any(Address.email_address.like('%foo%')))
143
144    # same, email address equals 'foo@bar.com'.  can fall back to keyword
145    # args for simple comparisons
146    filter(User.addresses.any(email_address = 'foo@bar.com'))
147
148    # return all Addresses whose user attribute has the username 'ed'
149    filter(Address.user.has(name='ed'))
150
151    # return all Addresses whose user attribute has the username 'ed'
152    # and an id > 5 (mixing clauses with kwargs)
153    filter(Address.user.has(User.id > 5, name='ed'))
154
155The ``Column`` collection remains available on mapped
156classes in the ``.c`` attribute.  Note that property-based
157expressions are only available with mapped properties of
158mapped classes.  ``.c`` is still used to access columns in
159regular tables and selectable objects produced from SQL
160Expressions.
161
162Automatic Join Aliasing
163^^^^^^^^^^^^^^^^^^^^^^^
164
165We've had join() and outerjoin() for a while now:
166
167::
168
169    session.query(Order).join('items')...
170
171Now you can alias them:
172
173::
174
175    session.query(Order).join('items', aliased=True).
176       filter(Item.name='item 1').join('items', aliased=True).filter(Item.name=='item 3')
177
178The above will create two joins from orders->items using
179aliases.  the ``filter()`` call subsequent to each will
180adjust its table criterion to that of the alias.  To get at
181the ``Item`` objects, use ``add_entity()`` and target each
182join with an ``id``:
183
184::
185
186    session.query(Order).join('items', id='j1', aliased=True).
187    filter(Item.name == 'item 1').join('items', aliased=True, id='j2').
188    filter(Item.name == 'item 3').add_entity(Item, id='j1').add_entity(Item, id='j2')
189
190Returns tuples in the form: ``(Order, Item, Item)``.
191
192Self-referential Queries
193^^^^^^^^^^^^^^^^^^^^^^^^
194
195So query.join() can make aliases now.  What does that give
196us ?  Self-referential queries !   Joins can be done without
197any ``Alias`` objects:
198
199::
200
201    # standard self-referential TreeNode mapper with backref
202    mapper(TreeNode, tree_nodes, properties={
203        'children':relation(TreeNode, backref=backref('parent', remote_side=tree_nodes.id))
204    })
205
206    # query for node with child containing "bar" two levels deep
207    session.query(TreeNode).join(["children", "children"], aliased=True).filter_by(name='bar')
208
209To add criterion for each table along the way in an aliased
210join, you can use ``from_joinpoint`` to keep joining against
211the same line of aliases:
212
213::
214
215    # search for the treenode along the path "n1/n12/n122"
216
217    # first find a Node with name="n122"
218    q = sess.query(Node).filter_by(name='n122')
219
220    # then join to parent with "n12"
221    q = q.join('parent', aliased=True).filter_by(name='n12')
222
223    # join again to the next parent with 'n1'.  use 'from_joinpoint'
224    # so we join from the previous point, instead of joining off the
225    # root table
226    q = q.join('parent', aliased=True, from_joinpoint=True).filter_by(name='n1')
227
228    node = q.first()
229
230``query.populate_existing()``
231^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
232
233The eager version of ``query.load()`` (or
234``session.refresh()``).  Every instance loaded from the
235query, including all eagerly loaded items, get refreshed
236immediately if already present in the session:
237
238::
239
240    session.query(Blah).populate_existing().all()
241
242Relations
243---------
244
245SQL Clauses Embedded in Updates/Inserts
246^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
247
248For inline execution of SQL clauses, embedded right in the
249UPDATE or INSERT, during a ``flush()``:
250
251::
252
253
254    myobject.foo = mytable.c.value + 1
255
256    user.pwhash = func.md5(password)
257
258    order.hash = text("select hash from hashing_table")
259
260The column-attribute is set up with a deferred loader after
261the operation, so that it issues the SQL to load the new
262value when you next access.
263
264Self-referential and Cyclical Eager Loading
265^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
266
267Since our alias-fu has improved, ``relation()`` can join
268along the same table \*any number of times*; you tell it how
269deep you want to go.  Lets show the self-referential
270``TreeNode`` more clearly:
271
272::
273
274    nodes = Table('nodes', metadata,
275         Column('id', Integer, primary_key=True),
276         Column('parent_id', Integer, ForeignKey('nodes.id')),
277         Column('name', String(30)))
278
279    class TreeNode(object):
280        pass
281
282    mapper(TreeNode, nodes, properties={
283        'children':relation(TreeNode, lazy=False, join_depth=3)
284    })
285
286So what happens when we say:
287
288::
289
290    create_session().query(TreeNode).all()
291
292?  A join along aliases, three levels deep off the parent:
293
294::
295
296    SELECT
297    nodes_3.id AS nodes_3_id, nodes_3.parent_id AS nodes_3_parent_id, nodes_3.name AS nodes_3_name,
298    nodes_2.id AS nodes_2_id, nodes_2.parent_id AS nodes_2_parent_id, nodes_2.name AS nodes_2_name,
299    nodes_1.id AS nodes_1_id, nodes_1.parent_id AS nodes_1_parent_id, nodes_1.name AS nodes_1_name,
300    nodes.id AS nodes_id, nodes.parent_id AS nodes_parent_id, nodes.name AS nodes_name
301    FROM nodes LEFT OUTER JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id
302    LEFT OUTER JOIN nodes AS nodes_2 ON nodes_1.id = nodes_2.parent_id
303    LEFT OUTER JOIN nodes AS nodes_3 ON nodes_2.id = nodes_3.parent_id
304    ORDER BY nodes.oid, nodes_1.oid, nodes_2.oid, nodes_3.oid
305
306Notice the nice clean alias names too.  The joining doesn't
307care if it's against the same immediate table or some other
308object which then cycles back to the beginning.  Any kind
309of chain of eager loads can cycle back onto itself when
310``join_depth`` is specified.  When not present, eager
311loading automatically stops when it hits a cycle.
312
313Composite Types
314^^^^^^^^^^^^^^^
315
316This is one from the Hibernate camp.  Composite Types let
317you define a custom datatype that is composed of more than
318one column (or one column, if you wanted).   Lets define a
319new type, ``Point``.  Stores an x/y coordinate:
320
321::
322
323    class Point(object):
324        def __init__(self, x, y):
325            self.x = x
326            self.y = y
327        def __composite_values__(self):
328            return self.x, self.y
329        def __eq__(self, other):
330            return other.x == self.x and other.y == self.y
331        def __ne__(self, other):
332            return not self.__eq__(other)
333
334The way the ``Point`` object is defined is specific to a
335custom type; constructor takes a list of arguments, and the
336``__composite_values__()`` method produces a sequence of
337those arguments.  The order will match up to our mapper, as
338we'll see in a moment.
339
340Let's create a table of vertices storing two points per row:
341
342::
343
344    vertices = Table('vertices', metadata,
345        Column('id', Integer, primary_key=True),
346        Column('x1', Integer),
347        Column('y1', Integer),
348        Column('x2', Integer),
349        Column('y2', Integer),
350        )
351
352Then, map it !  We'll create a ``Vertex`` object which
353stores two ``Point`` objects:
354
355::
356
357    class Vertex(object):
358        def __init__(self, start, end):
359            self.start = start
360            self.end = end
361
362    mapper(Vertex, vertices, properties={
363        'start':composite(Point, vertices.c.x1, vertices.c.y1),
364        'end':composite(Point, vertices.c.x2, vertices.c.y2)
365    })
366
367Once you've set up your composite type, it's usable just
368like any other type:
369
370::
371
372
373    v = Vertex(Point(3, 4), Point(26,15))
374    session.save(v)
375    session.flush()
376
377    # works in queries too
378    q = session.query(Vertex).filter(Vertex.start == Point(3, 4))
379
380If you'd like to define the way the mapped attributes
381generate SQL clauses when used in expressions, create your
382own ``sqlalchemy.orm.PropComparator`` subclass, defining any
383of the common operators (like ``__eq__()``, ``__le__()``,
384etc.), and send it in to ``composite()``.  Composite types
385work as primary keys too, and are usable in ``query.get()``:
386
387::
388
389    # a Document class which uses a composite Version
390    # object as primary key
391    document = query.get(Version(1, 'a'))
392
393``dynamic_loader()`` relations
394^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
395
396A ``relation()`` that returns a live ``Query`` object for
397all read operations.  Write operations are limited to just
398``append()`` and ``remove()``, changes to the collection are
399not visible until the session is flushed.  This feature is
400particularly handy with an "autoflushing" session which will
401flush before each query.
402
403::
404
405    mapper(Foo, foo_table, properties={
406        'bars':dynamic_loader(Bar, backref='foo', <other relation() opts>)
407    })
408
409    session = create_session(autoflush=True)
410    foo = session.query(Foo).first()
411
412    foo.bars.append(Bar(name='lala'))
413
414    for bar in foo.bars.filter(Bar.name=='lala'):
415        print(bar)
416
417    session.commit()
418
419New Options: ``undefer_group()``, ``eagerload_all()``
420^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
421
422A couple of query options which are handy.
423``undefer_group()`` marks a whole group of "deferred"
424columns as undeferred:
425
426::
427
428    mapper(Class, table, properties={
429        'foo' : deferred(table.c.foo, group='group1'),
430        'bar' : deferred(table.c.bar, group='group1'),
431        'bat' : deferred(table.c.bat, group='group1'),
432    )
433
434    session.query(Class).options(undefer_group('group1')).filter(...).all()
435
436and ``eagerload_all()`` sets a chain of attributes to be
437eager in one pass:
438
439::
440
441    mapper(Foo, foo_table, properties={
442       'bar':relation(Bar)
443    })
444    mapper(Bar, bar_table, properties={
445       'bat':relation(Bat)
446    })
447    mapper(Bat, bat_table)
448
449    # eager load bar and bat
450    session.query(Foo).options(eagerload_all('bar.bat')).filter(...).all()
451
452New Collection API
453^^^^^^^^^^^^^^^^^^
454
455Collections are no longer proxied by an
456{{{InstrumentedList}}} proxy, and access to members, methods
457and attributes is direct.   Decorators now intercept objects
458entering and leaving the collection, and it is now possible
459to easily write a custom collection class that manages its
460own membership.  Flexible decorators also replace the named
461method interface of custom collections in 0.3, allowing any
462class to be easily adapted to use as a collection container.
463
464Dictionary-based collections are now much easier to use and
465fully ``dict``-like.  Changing ``__iter__`` is no longer
466needed for ``dict``s, and new built-in ``dict`` types cover
467many needs:
468
469::
470
471    # use a dictionary relation keyed by a column
472    relation(Item, collection_class=column_mapped_collection(items.c.keyword))
473    # or named attribute
474    relation(Item, collection_class=attribute_mapped_collection('keyword'))
475    # or any function you like
476    relation(Item, collection_class=mapped_collection(lambda entity: entity.a + entity.b))
477
478Existing 0.3 ``dict``-like and freeform object derived
479collection classes will need to be updated for the new API.
480In most cases this is simply a matter of adding a couple
481decorators to the class definition.
482
483Mapped Relations from External Tables/Subqueries
484^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
485
486This feature quietly appeared in 0.3 but has been improved
487in 0.4 thanks to better ability to convert subqueries
488against a table into subqueries against an alias of that
489table; this is key for eager loading, aliased joins in
490queries, etc.  It reduces the need to create mappers against
491select statements when you just need to add some extra
492columns or subqueries:
493
494::
495
496    mapper(User, users, properties={
497           'fullname': column_property((users.c.firstname + users.c.lastname).label('fullname')),
498           'numposts': column_property(
499                select([func.count(1)], users.c.id==posts.c.user_id).correlate(users).label('posts')
500           )
501        })
502
503a typical query looks like:
504
505::
506
507    SELECT (SELECT count(1) FROM posts WHERE users.id = posts.user_id) AS count,
508    users.firstname || users.lastname AS fullname,
509    users.id AS users_id, users.firstname AS users_firstname, users.lastname AS users_lastname
510    FROM users ORDER BY users.oid
511
512Horizontal Scaling (Sharding) API
513---------------------------------
514
515[browser:/sqlalchemy/trunk/examples/sharding/attribute_shard
516.py]
517
518Sessions
519--------
520
521New Session Create Paradigm; SessionContext, assignmapper Deprecated
522^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
523
524That's right, the whole shebang is being replaced with two
525configurational functions.  Using both will produce the most
5260.1-ish feel we've had since 0.1 (i.e., the least amount of
527typing).
528
529Configure your own ``Session`` class right where you define
530your ``engine`` (or anywhere):
531
532::
533
534    from sqlalchemy import create_engine
535    from sqlalchemy.orm import sessionmaker
536
537    engine = create_engine('myengine://')
538    Session = sessionmaker(bind=engine, autoflush=True, transactional=True)
539
540    # use the new Session() freely
541    sess = Session()
542    sess.save(someobject)
543    sess.flush()
544
545
546If you need to post-configure your Session, say with an
547engine, add it later with ``configure()``:
548
549::
550
551    Session.configure(bind=create_engine(...))
552
553All the behaviors of ``SessionContext`` and the ``query``
554and ``__init__`` methods of ``assignmapper`` are moved into
555the new ``scoped_session()`` function, which is compatible
556with both ``sessionmaker`` as well as ``create_session()``:
557
558::
559
560    from sqlalchemy.orm import scoped_session, sessionmaker
561
562    Session = scoped_session(sessionmaker(autoflush=True, transactional=True))
563    Session.configure(bind=engine)
564
565    u = User(name='wendy')
566
567    sess = Session()
568    sess.save(u)
569    sess.commit()
570
571    # Session constructor is thread-locally scoped.  Everyone gets the same
572    # Session in the thread when scope="thread".
573    sess2 = Session()
574    assert sess is sess2
575
576
577When using a thread-local ``Session``, the returned class
578has all of ``Session's`` interface implemented as
579classmethods, and "assignmapper"'s functionality is
580available using the ``mapper`` classmethod.  Just like the
581old ``objectstore`` days....
582
583::
584
585
586    # "assignmapper"-like functionality available via ScopedSession.mapper
587    Session.mapper(User, users_table)
588
589    u = User(name='wendy')
590
591    Session.commit()
592
593
594Sessions are again Weak Referencing By Default
595^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
596
597The weak_identity_map flag is now set to ``True`` by default
598on Session.  Instances which are externally deferenced and
599fall out of scope are removed from the session
600automatically.   However, items which have "dirty" changes
601present will remain strongly referenced until those changes
602are flushed at which case the object reverts to being weakly
603referenced (this works for 'mutable' types, like picklable
604attributes, as well).  Setting weak_identity_map to
605``False`` restores the old strong-referencing behavior for
606those of you using the session like a cache.
607
608Auto-Transactional Sessions
609^^^^^^^^^^^^^^^^^^^^^^^^^^^
610
611As you might have noticed above, we are calling ``commit()``
612on ``Session``.  The flag ``transactional=True`` means the
613``Session`` is always in a transaction, ``commit()``
614persists permanently.
615
616Auto-Flushing Sessions
617^^^^^^^^^^^^^^^^^^^^^^
618
619Also, ``autoflush=True`` means the ``Session`` will
620``flush()`` before each ``query`` as well as when you call
621``flush()`` or ``commit()``.  So now this will work:
622
623::
624
625    Session = sessionmaker(bind=engine, autoflush=True, transactional=True)
626
627    u = User(name='wendy')
628
629    sess = Session()
630    sess.save(u)
631
632    # wendy is flushed, comes right back from a query
633    wendy = sess.query(User).filter_by(name='wendy').one()
634
635Transactional methods moved onto sessions
636^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
637
638``commit()`` and ``rollback()``, as well as ``begin()`` are
639now directly on ``Session``.  No more need to use
640``SessionTransaction`` for anything (it remains in the
641background).
642
643::
644
645    Session = sessionmaker(autoflush=True, transactional=False)
646
647    sess = Session()
648    sess.begin()
649
650    # use the session
651
652    sess.commit() # commit transaction
653
654Sharing a ``Session`` with an enclosing engine-level (i.e.
655non-ORM) transaction is easy:
656
657::
658
659    Session = sessionmaker(autoflush=True, transactional=False)
660
661    conn = engine.connect()
662    trans = conn.begin()
663    sess = Session(bind=conn)
664
665    # ... session is transactional
666
667    # commit the outermost transaction
668    trans.commit()
669
670Nested Session Transactions with SAVEPOINT
671^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
672
673Available at the Engine and ORM level.  ORM docs so far:
674
675http://www.sqlalchemy.org/docs/04/session.html#unitofwork_ma
676naging
677
678Two-Phase Commit Sessions
679^^^^^^^^^^^^^^^^^^^^^^^^^
680
681Available at the Engine and ORM level.  ORM docs so far:
682
683http://www.sqlalchemy.org/docs/04/session.html#unitofwork_ma
684naging
685
686Inheritance
687-----------
688
689Polymorphic Inheritance with No Joins or Unions
690^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
691
692New docs for inheritance:  http://www.sqlalchemy.org/docs/04
693/mappers.html#advdatamapping_mapper_inheritance_joined
694
695Better Polymorphic Behavior with ``get()``
696^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
697
698All classes within a joined-table inheritance hierarchy get
699an ``_instance_key`` using the base class, i.e.
700``(BaseClass, (1, ), None)``.  That way when you call
701``get()`` a ``Query`` against the base class, it can locate
702subclass instances in the current identity map without
703querying the database.
704
705Types
706-----
707
708Custom Subclasses of ``sqlalchemy.types.TypeDecorator``
709^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
710
711There is a `New API <http://www.sqlalchemy.org/docs/04/types
712.html#types_custom>`_ for subclassing a TypeDecorator.
713Using the 0.3 API causes compilation errors in some cases.
714
715SQL Expressions
716===============
717
718All New, Deterministic Label/Alias Generation
719---------------------------------------------
720
721All the "anonymous" labels and aliases use a simple
722<name>_<number> format now.  SQL is much easier to read and
723is compatible with plan optimizer caches.  Just check out
724some of the examples in the tutorials:
725http://www.sqlalchemy.org/docs/04/ormtutorial.html
726http://www.sqlalchemy.org/docs/04/sqlexpression.html
727
728Generative select() Constructs
729------------------------------
730
731This is definitely the way to go with ``select()``.  See htt
732p://www.sqlalchemy.org/docs/04/sqlexpression.html#sql_transf
733orm .
734
735New Operator System
736-------------------
737
738SQL operators and more or less every SQL keyword there is
739are now abstracted into the compiler layer.  They now act
740intelligently and are type/backend aware, see: http://www.sq
741lalchemy.org/docs/04/sqlexpression.html#sql_operators
742
743All ``type`` Keyword Arguments Renamed to ``type_``
744---------------------------------------------------
745
746Just like it says:
747
748::
749
750       b = bindparam('foo', type_=String)
751
752in\_ Function Changed to Accept Sequence or Selectable
753------------------------------------------------------
754
755The in\_ function now takes a sequence of values or a
756selectable as its sole argument. The previous API of passing
757in values as positional arguments still works, but is now
758deprecated. This means that
759
760::
761
762    my_table.select(my_table.c.id.in_(1,2,3)
763    my_table.select(my_table.c.id.in_(*listOfIds)
764
765should be changed to
766
767::
768
769    my_table.select(my_table.c.id.in_([1,2,3])
770    my_table.select(my_table.c.id.in_(listOfIds)
771
772Schema and Reflection
773=====================
774
775``MetaData``, ``BoundMetaData``, ``DynamicMetaData``...
776-------------------------------------------------------
777
778In the 0.3.x series, ``BoundMetaData`` and
779``DynamicMetaData`` were deprecated in favor of ``MetaData``
780and ``ThreadLocalMetaData``.  The older names have been
781removed in 0.4.  Updating is simple:
782
783::
784
785    +-------------------------------------+-------------------------+
786    |If You Had                           | Now Use                 |
787    +=====================================+=========================+
788    | ``MetaData``                        | ``MetaData``            |
789    +-------------------------------------+-------------------------+
790    | ``BoundMetaData``                   | ``MetaData``            |
791    +-------------------------------------+-------------------------+
792    | ``DynamicMetaData`` (with one       | ``MetaData``            |
793    | engine or threadlocal=False)        |                         |
794    +-------------------------------------+-------------------------+
795    | ``DynamicMetaData``                 | ``ThreadLocalMetaData`` |
796    | (with different engines per thread) |                         |
797    +-------------------------------------+-------------------------+
798
799The seldom-used ``name`` parameter to ``MetaData`` types has
800been removed.  The ``ThreadLocalMetaData`` constructor now
801takes no arguments.  Both types can now be bound to an
802``Engine`` or a single ``Connection``.
803
804One Step Multi-Table Reflection
805-------------------------------
806
807You can now load table definitions and automatically create
808``Table`` objects from an entire database or schema in one
809pass:
810
811::
812
813    >>> metadata = MetaData(myengine, reflect=True)
814    >>> metadata.tables.keys()
815    ['table_a', 'table_b', 'table_c', '...']
816
817``MetaData`` also gains a ``.reflect()`` method enabling
818finer control over the loading process, including
819specification of a subset of available tables to load.
820
821SQL Execution
822=============
823
824``engine``, ``connectable``, and ``bind_to`` are all now ``bind``
825-----------------------------------------------------------------
826
827``Transactions``, ``NestedTransactions`` and ``TwoPhaseTransactions``
828---------------------------------------------------------------------
829
830Connection Pool Events
831----------------------
832
833The connection pool now fires events when new DB-API
834connections are created, checked out and checked back into
835the pool.   You can use these to execute session-scoped SQL
836setup statements on fresh connections, for example.
837
838Oracle Engine Fixed
839-------------------
840
841In 0.3.11, there were bugs in the Oracle Engine on how
842Primary Keys are handled.  These bugs could cause programs
843that worked fine with other engines, such as sqlite, to fail
844when using the Oracle Engine.  In 0.4, the Oracle Engine has
845been reworked, fixing these Primary Key problems.
846
847Out Parameters for Oracle
848-------------------------
849
850::
851
852    result = engine.execute(text("begin foo(:x, :y, :z); end;", bindparams=[bindparam('x', Numeric), outparam('y', Numeric), outparam('z', Numeric)]), x=5)
853    assert result.out_parameters == {'y':10, 'z':75}
854
855Connection-bound ``MetaData``, ``Sessions``
856-------------------------------------------
857
858``MetaData`` and ``Session`` can be explicitly bound to a
859connection:
860
861::
862
863    conn = engine.connect()
864    sess = create_session(bind=conn)
865
866Faster, More Foolproof ``ResultProxy`` Objects
867----------------------------------------------
868
869