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 sqlalchemy import *``" would import all of
49sqlalchemy'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
675https://www.sqlalchemy.org/docs/04/session.html#unitofwork_managing
676
677Two-Phase Commit Sessions
678^^^^^^^^^^^^^^^^^^^^^^^^^
679
680Available at the Engine and ORM level.  ORM docs so far:
681
682https://www.sqlalchemy.org/docs/04/session.html#unitofwork_managing
683
684Inheritance
685-----------
686
687Polymorphic Inheritance with No Joins or Unions
688^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
689
690New docs for inheritance:  https://www.sqlalchemy.org/docs/04
691/mappers.html#advdatamapping_mapper_inheritance_joined
692
693Better Polymorphic Behavior with ``get()``
694^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
695
696All classes within a joined-table inheritance hierarchy get
697an ``_instance_key`` using the base class, i.e.
698``(BaseClass, (1, ), None)``.  That way when you call
699``get()`` a ``Query`` against the base class, it can locate
700subclass instances in the current identity map without
701querying the database.
702
703Types
704-----
705
706Custom Subclasses of ``sqlalchemy.types.TypeDecorator``
707^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
708
709There is a `New API <https://www.sqlalchemy.org/docs/04/types
710.html#types_custom>`_ for subclassing a TypeDecorator.
711Using the 0.3 API causes compilation errors in some cases.
712
713SQL Expressions
714===============
715
716All New, Deterministic Label/Alias Generation
717---------------------------------------------
718
719All the "anonymous" labels and aliases use a simple
720<name>_<number> format now.  SQL is much easier to read and
721is compatible with plan optimizer caches.  Just check out
722some of the examples in the tutorials:
723https://www.sqlalchemy.org/docs/04/ormtutorial.html
724https://www.sqlalchemy.org/docs/04/sqlexpression.html
725
726Generative select() Constructs
727------------------------------
728
729This is definitely the way to go with ``select()``.  See htt
730p://www.sqlalchemy.org/docs/04/sqlexpression.html#sql_transf
731orm .
732
733New Operator System
734-------------------
735
736SQL operators and more or less every SQL keyword there is
737are now abstracted into the compiler layer.  They now act
738intelligently and are type/backend aware, see:
739https://www.sqlalchemy.org/docs/04/sqlexpression.html#sql_operators
740
741All ``type`` Keyword Arguments Renamed to ``type_``
742---------------------------------------------------
743
744Just like it says:
745
746::
747
748       b = bindparam('foo', type_=String)
749
750in\_ Function Changed to Accept Sequence or Selectable
751------------------------------------------------------
752
753The in\_ function now takes a sequence of values or a
754selectable as its sole argument. The previous API of passing
755in values as positional arguments still works, but is now
756deprecated. This means that
757
758::
759
760    my_table.select(my_table.c.id.in_(1,2,3)
761    my_table.select(my_table.c.id.in_(*listOfIds)
762
763should be changed to
764
765::
766
767    my_table.select(my_table.c.id.in_([1,2,3])
768    my_table.select(my_table.c.id.in_(listOfIds)
769
770Schema and Reflection
771=====================
772
773``MetaData``, ``BoundMetaData``, ``DynamicMetaData``...
774-------------------------------------------------------
775
776In the 0.3.x series, ``BoundMetaData`` and
777``DynamicMetaData`` were deprecated in favor of ``MetaData``
778and ``ThreadLocalMetaData``.  The older names have been
779removed in 0.4.  Updating is simple:
780
781::
782
783    +-------------------------------------+-------------------------+
784    |If You Had                           | Now Use                 |
785    +=====================================+=========================+
786    | ``MetaData``                        | ``MetaData``            |
787    +-------------------------------------+-------------------------+
788    | ``BoundMetaData``                   | ``MetaData``            |
789    +-------------------------------------+-------------------------+
790    | ``DynamicMetaData`` (with one       | ``MetaData``            |
791    | engine or threadlocal=False)        |                         |
792    +-------------------------------------+-------------------------+
793    | ``DynamicMetaData``                 | ``ThreadLocalMetaData`` |
794    | (with different engines per thread) |                         |
795    +-------------------------------------+-------------------------+
796
797The seldom-used ``name`` parameter to ``MetaData`` types has
798been removed.  The ``ThreadLocalMetaData`` constructor now
799takes no arguments.  Both types can now be bound to an
800``Engine`` or a single ``Connection``.
801
802One Step Multi-Table Reflection
803-------------------------------
804
805You can now load table definitions and automatically create
806``Table`` objects from an entire database or schema in one
807pass:
808
809::
810
811    >>> metadata = MetaData(myengine, reflect=True)
812    >>> metadata.tables.keys()
813    ['table_a', 'table_b', 'table_c', '...']
814
815``MetaData`` also gains a ``.reflect()`` method enabling
816finer control over the loading process, including
817specification of a subset of available tables to load.
818
819SQL Execution
820=============
821
822``engine``, ``connectable``, and ``bind_to`` are all now ``bind``
823-----------------------------------------------------------------
824
825``Transactions``, ``NestedTransactions`` and ``TwoPhaseTransactions``
826---------------------------------------------------------------------
827
828Connection Pool Events
829----------------------
830
831The connection pool now fires events when new DB-API
832connections are created, checked out and checked back into
833the pool.   You can use these to execute session-scoped SQL
834setup statements on fresh connections, for example.
835
836Oracle Engine Fixed
837-------------------
838
839In 0.3.11, there were bugs in the Oracle Engine on how
840Primary Keys are handled.  These bugs could cause programs
841that worked fine with other engines, such as sqlite, to fail
842when using the Oracle Engine.  In 0.4, the Oracle Engine has
843been reworked, fixing these Primary Key problems.
844
845Out Parameters for Oracle
846-------------------------
847
848::
849
850    result = engine.execute(text("begin foo(:x, :y, :z); end;", bindparams=[bindparam('x', Numeric), outparam('y', Numeric), outparam('z', Numeric)]), x=5)
851    assert result.out_parameters == {'y':10, 'z':75}
852
853Connection-bound ``MetaData``, ``Sessions``
854-------------------------------------------
855
856``MetaData`` and ``Session`` can be explicitly bound to a
857connection:
858
859::
860
861    conn = engine.connect()
862    sess = create_session(bind=conn)
863
864Faster, More Foolproof ``ResultProxy`` Objects
865----------------------------------------------
866
867